├── src └── AzureFunctions.Extensions.Swashbuckle │ ├── TestFunction │ ├── host.json │ ├── server.pfx │ ├── local.settings.json │ ├── CustomFilterExample │ │ └── RemoveSchemasFilter.cs │ ├── Models │ │ └── TestModel.cs │ ├── TestFunction.csproj │ ├── SwaggerController.cs │ ├── Program.cs │ ├── .gitignore │ ├── TestFunction.xml │ └── TestController.cs │ ├── AzureFunctions.Extensions.Swashbuckle │ ├── Attribute │ │ ├── SwaggerIgnoreAttribute.cs │ │ ├── SupportedRequestFormatAttribute.cs │ │ ├── RequestHttpHeaderAttribute.cs │ │ ├── SwaggerUploadFileAttribute.cs │ │ ├── RequestBodyTypeAttribute.cs │ │ └── QueryStringParameterAttribute.cs │ ├── EmbededResources │ │ └── resources.zip │ ├── SwashBuckle │ │ ├── SwashBuckleStartupConfig.cs │ │ ├── Hosting │ │ │ └── FunctionHostingEnvironment.cs │ │ ├── Filters │ │ │ ├── GenerateOperationIdFilter.cs │ │ │ ├── FunctionsOperationFilter.cs │ │ │ ├── QueryStringParameterAttributeFilter.cs │ │ │ ├── XmlCommentsSchemaFilterChanged.cs │ │ │ ├── Mapper │ │ │ │ ├── JsonMapper.cs │ │ │ │ └── TypeMapper.cs │ │ │ ├── FileUploadOperationFilter.cs │ │ │ ├── XmlCommentsParameterFilterWithExamples.cs │ │ │ └── XmlCommentsOperationFilterWithParams.cs │ │ ├── Extensions │ │ │ └── AssemblyExtensions.cs │ │ ├── SwashBuckleClient.cs │ │ ├── SwashbuckleConfig.cs │ │ └── Providers │ │ │ └── FunctionApiDescriptionProvider.cs │ ├── Settings │ │ ├── SwaggerDocument.cs │ │ └── SwaggerDocOptions.cs │ ├── ISwashBuckleClient.cs │ ├── SwashBuckleStartupExtension.cs │ ├── SwashBuckleClientExtension.cs │ └── AzureFunctions.Extensions.Swashbuckle.csproj │ ├── AzureFunctions.Extensions.Swashbuckle.sln.DotSettings │ ├── AzureFunctions.Extensions.Swashbuckle.sln │ └── .editorconfig ├── stylecop.json ├── LICENSE ├── StyleCop.ruleset ├── .gitignore └── README.md /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "http": { 5 | "routePrefix": "api" 6 | } 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/server.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitalybibikov/AzureExtensions.Swashbuckle/HEAD/src/AzureFunctions.Extensions.Swashbuckle/TestFunction/server.pfx -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Attribute/SwaggerIgnoreAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace AzureFunctions.Extensions.Swashbuckle.Attribute 2 | { 3 | public class SwaggerIgnoreAttribute : System.Attribute 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/EmbededResources/resources.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitalybibikov/AzureExtensions.Swashbuckle/HEAD/src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/EmbededResources/resources.zip -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/SwashBuckleStartupConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle 4 | { 5 | public sealed class SwashBuckleStartupConfig 6 | { 7 | public Assembly Assembly { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "UseDevelopmentStorage=true", 5 | "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" 6 | }, 7 | 8 | "SwaggerDocOptions": { 9 | "Title": "Test title with bug", 10 | "XmlPath": "TestFunction.xml", 11 | "PrependOperationWithRoutePrefix": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Settings/SwaggerDocument.cs: -------------------------------------------------------------------------------- 1 | namespace AzureFunctions.Extensions.Swashbuckle.Settings 2 | { 3 | public class SwaggerDocument 4 | { 5 | public string Name { get; set; } = "v1"; 6 | 7 | public string Title { get; set; } = "Swashbuckle"; 8 | 9 | public string Version { get; set; } = "v1"; 10 | 11 | public string Description { get; set; } = "Swagger document by Swashbuckle"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | DO_NOT_SHOW -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/ISwashBuckleClient.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace AzureFunctions.Extensions.Swashbuckle 4 | { 5 | public interface ISwashBuckleClient 6 | { 7 | string RoutePrefix { get; } 8 | Stream GetSwaggerJsonDocument(string host, string documentName = "v1"); 9 | Stream GetSwaggerYamlDocument(string host, string documentName = "v1"); 10 | Stream GetSwaggerUi(string swaggerUrl); 11 | Stream GetSwaggerOAuth2Redirect(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Attribute/SupportedRequestFormatAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureFunctions.Extensions.Swashbuckle.Attribute 4 | { 5 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 6 | public class SupportedRequestFormatAttribute : System.Attribute 7 | { 8 | public SupportedRequestFormatAttribute(string mediaType) 9 | { 10 | MediaType = mediaType; 11 | } 12 | 13 | public string MediaType { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/CustomFilterExample/RemoveSchemasFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.OpenApi.Models; 4 | using Swashbuckle.AspNetCore.SwaggerGen; 5 | 6 | namespace TestFunction.CustomFilterExample 7 | { 8 | public class RemoveSchemasFilter : IDocumentFilter 9 | { 10 | public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) 11 | { 12 | foreach (var item in swaggerDoc.Components.Schemas) 13 | { 14 | swaggerDoc.Components.Schemas.Remove(item.Key); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Attribute/RequestHttpHeaderAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureFunctions.Extensions.Swashbuckle.Attribute 4 | { 5 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] 6 | public class RequestHttpHeaderAttribute : System.Attribute 7 | { 8 | public RequestHttpHeaderAttribute(string headerName, bool isRequired = false) 9 | { 10 | HeaderName = headerName; 11 | IsRequired = isRequired; 12 | } 13 | 14 | public string HeaderName { get; } 15 | 16 | public bool IsRequired { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Hosting/FunctionHostingEnvironment.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.FileProviders; 3 | 4 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Hosting 5 | { 6 | internal class FunctionHostingEnvironment : IWebHostEnvironment 7 | { 8 | public string ApplicationName { get; set; } 9 | public IFileProvider ContentRootFileProvider { get; set; } 10 | public string ContentRootPath { get; set; } 11 | public string EnvironmentName { get; set; } 12 | public IFileProvider WebRootFileProvider { get; set; } 13 | public string WebRootPath { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Attribute/SwaggerUploadFileAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureFunctions.Extensions.Swashbuckle.Attribute 4 | { 5 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] 6 | public class SwaggerUploadFileAttribute : System.Attribute 7 | { 8 | public SwaggerUploadFileAttribute(string name, string description, string example = "") 9 | { 10 | this.Name = name; 11 | this.Description = description; 12 | this.Example = example; 13 | } 14 | 15 | public string Name { get; } 16 | 17 | public string Description { get; set; } 18 | 19 | public string Example { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/GenerateOperationIdFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Controllers; 2 | using Microsoft.OpenApi.Models; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters 6 | { 7 | internal class GenerateOperationIdFilter : IOperationFilter 8 | { 9 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 10 | { 11 | if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor descriptor 12 | && !string.IsNullOrEmpty(descriptor 13 | .ActionName)) 14 | { 15 | operation.OperationId = 16 | descriptor.ActionName; 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Extensions/AssemblyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Extensions 6 | { 7 | internal static class AssemblyExtensions 8 | { 9 | public static Stream? GetResourceByName(this Assembly assembly, string name) 10 | { 11 | if (assembly == null) 12 | { 13 | throw new ArgumentNullException(nameof(assembly)); 14 | } 15 | 16 | if (assembly == null) 17 | { 18 | throw new ArgumentNullException(nameof(assembly)); 19 | } 20 | 21 | return assembly.GetManifestResourceStream($"{typeof(ISwashBuckleClient).Namespace}.{name}"); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/Models/TestModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace TestFunction.Models 4 | { 5 | /// 6 | /// Some model 7 | /// 8 | public class TestModel 9 | { 10 | /// 11 | /// Id (example) 12 | /// 13 | /// 3 14 | [Required] 15 | public int Id { get; set; } 16 | 17 | /// 18 | /// Name (example) 19 | /// 20 | /// John 21 | [Required] 22 | [MaxLength(512)] 23 | public string Name { get; set; } 24 | 25 | /// 26 | /// Description (example) 27 | /// 28 | /// Sometimes human 29 | [MaxLength(10240)] 30 | public string Description { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Vitali Bibikov", 6 | "copyrightText": "Copyright (c) {Vitali Bibikov}. All Rights Reserved.\r\n.", 7 | "xmlHeader": false, 8 | "fileNamingConvention": "metadata" 9 | }, 10 | "layoutRules": { 11 | "newlineAtEndOfFile": "require" 12 | }, 13 | "spacingRules": {}, 14 | "readabilityRules": {}, 15 | "orderingRules": { 16 | "usingDirectivesPlacement": "outsideNamespace", 17 | "blankLinesBetweenUsingGroups": "omit" 18 | }, 19 | "indentation": { 20 | "indentationSize": 4, 21 | "tabSize": 4, 22 | "useTabs": false 23 | }, 24 | "maintainabilityRules": {} 25 | } 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vitali Bibikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Attribute/RequestBodyTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace AzureFunctions.Extensions.Swashbuckle.Attribute 5 | { 6 | /// 7 | /// Explicit body type definition for functions with as input parameter 8 | /// 9 | [AttributeUsage(AttributeTargets.Parameter)] 10 | public class RequestBodyTypeAttribute : System.Attribute 11 | { 12 | /// 13 | /// Explicit body type definition for functions with as input parameter 14 | /// 15 | /// Body model type 16 | /// Model description 17 | public RequestBodyTypeAttribute(Type bodyType, string description) 18 | { 19 | this.Type = bodyType; 20 | this.Description = description; 21 | } 22 | 23 | /// 24 | /// Gets Body model type 25 | /// 26 | public Type Type { get; } 27 | 28 | /// 29 | /// Gets Body model description 30 | /// 31 | public string Description { get; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Settings/SwaggerDocOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Microsoft.OpenApi; 3 | using Swashbuckle.AspNetCore.SwaggerGen; 4 | 5 | namespace AzureFunctions.Extensions.Swashbuckle.Settings 6 | { 7 | public class SwaggerDocOptions 8 | { 9 | public string? Title { get; set; } = Assembly.GetAssembly(typeof(SwaggerDocOptions))?.GetName().Name; 10 | 11 | public string? XmlPath { get; set; } 12 | 13 | public bool AddCodeParameter { get; set; } = true; 14 | 15 | public IEnumerable Documents { get; set; } = new List(); 16 | 17 | public bool PrependOperationWithRoutePrefix { get; set; } = true; 18 | 19 | public Uri OverridenPathToSwaggerJson { get; set; } = default!; 20 | 21 | public OpenApiSpecVersion SpecVersion { get; set; } = OpenApiSpecVersion.OpenApi3_0; 22 | 23 | public Action? ConfigureSwaggerGen { get; set; } 24 | 25 | public string ClientId { get; set; } = string.Empty; 26 | 27 | public string ClientSecret { get; set; } = string.Empty; 28 | 29 | public string OAuth2RedirectPath { get; set; } = string.Empty; 30 | 31 | public bool UseBasicAuthenticationWithAccessCodeGrant { get; set; } = false; 32 | 33 | public bool UsePkceWithAuthorizationCodeGrant { get; set; } = false; 34 | 35 | public bool AddNewtonsoftSupport { get; set; } = false; 36 | 37 | public string RoutePrefix { get; set; } = string.Empty; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/TestFunction.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | v4 5 | Exe 6 | 7 | 8 | 9 | TestFunction.xml 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | PreserveNewest 31 | 32 | 33 | PreserveNewest 34 | Never 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/SwashBuckleClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle 5 | { 6 | public sealed class SwashBuckleClient : ISwashBuckleClient 7 | { 8 | private readonly SwashbuckleConfig _config; 9 | 10 | public SwashBuckleClient(SwashbuckleConfig config) 11 | { 12 | _config = config; 13 | } 14 | 15 | public Stream GetSwaggerJsonDocument(string host, string documentName = "v1") 16 | { 17 | return _config.GetSwaggerJsonDocument(host, documentName); 18 | } 19 | 20 | public Stream GetSwaggerYamlDocument(string host, string documentName = "v1") 21 | { 22 | return _config.GetSwaggerYamlDocument(host, documentName); 23 | } 24 | 25 | public Stream GetSwaggerOAuth2Redirect() 26 | { 27 | var content = _config.GetSwaggerOAuth2RedirectContent(); 28 | var memoryStream = new MemoryStream(); 29 | var writer = new StreamWriter(memoryStream); 30 | writer.Write(content); 31 | writer.Flush(); 32 | memoryStream.Seek(0, SeekOrigin.Begin); 33 | return memoryStream; 34 | } 35 | 36 | public Stream GetSwaggerUi(string swaggerUrl) 37 | { 38 | var memoryStream = new MemoryStream(); 39 | var writer = new StreamWriter(memoryStream); 40 | 41 | writer.Write(_config.GetSwaggerUIContent(swaggerUrl)); 42 | writer.Flush(); 43 | 44 | memoryStream.Position = 0; 45 | return memoryStream; 46 | } 47 | 48 | public string RoutePrefix => _config.RoutePrefix; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34607.119 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctions.Extensions.Swashbuckle", "AzureFunctions.Extensions.Swashbuckle\AzureFunctions.Extensions.Swashbuckle.csproj", "{1E41B7D0-91CC-4232-88E8-ED5330EDB7BF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFunction", "TestFunction\TestFunction.csproj", "{93B84BC0-2017-4BC2-8191-20EDF594E7AC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {1E41B7D0-91CC-4232-88E8-ED5330EDB7BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {1E41B7D0-91CC-4232-88E8-ED5330EDB7BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {1E41B7D0-91CC-4232-88E8-ED5330EDB7BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {1E41B7D0-91CC-4232-88E8-ED5330EDB7BF}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {93B84BC0-2017-4BC2-8191-20EDF594E7AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {93B84BC0-2017-4BC2-8191-20EDF594E7AC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {93B84BC0-2017-4BC2-8191-20EDF594E7AC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {93B84BC0-2017-4BC2-8191-20EDF594E7AC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {34BB311A-992E-44A4-8F07-34B7D23F8139} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/FunctionsOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using AzureFunctions.Extensions.Swashbuckle.Attribute; 3 | using Microsoft.OpenApi.Models; 4 | using Swashbuckle.AspNetCore.SwaggerGen; 5 | 6 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters 7 | { 8 | internal class FunctionsOperationFilter : IOperationFilter 9 | { 10 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 11 | { 12 | operation.Parameters ??= new List 13 | { 14 | Capacity = 4 15 | }; 16 | 17 | foreach (var customAttribute in context.MethodInfo.GetCustomAttributes(typeof(RequestHttpHeaderAttribute), false)) 18 | { 19 | operation.Parameters.Add(new OpenApiParameter 20 | { 21 | Name = (customAttribute as RequestHttpHeaderAttribute)!.HeaderName, 22 | In = ParameterLocation.Header, 23 | Schema = new OpenApiSchema { Type = "string" }, 24 | Required = (customAttribute as RequestHttpHeaderAttribute)!.IsRequired 25 | }); 26 | } 27 | 28 | foreach (var customAttribute in context.MethodInfo.DeclaringType!.GetCustomAttributes( 29 | typeof(RequestHttpHeaderAttribute), false)) 30 | { 31 | operation.Parameters.Add(new OpenApiParameter 32 | { 33 | Name = (customAttribute as RequestHttpHeaderAttribute)!.HeaderName, 34 | In = ParameterLocation.Header, 35 | Schema = new OpenApiSchema { Type = "string" }, 36 | Required = (customAttribute as RequestHttpHeaderAttribute)!.IsRequired 37 | }); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /StyleCop.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/QueryStringParameterAttributeFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using AzureFunctions.Extensions.Swashbuckle.Attribute; 3 | using Microsoft.OpenApi.Any; 4 | using Microsoft.OpenApi.Models; 5 | using Swashbuckle.AspNetCore.SwaggerGen; 6 | 7 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters 8 | { 9 | internal class QueryStringParameterAttributeFilter : IOperationFilter 10 | { 11 | private readonly ISchemaGenerator schemaGenerator; 12 | 13 | public QueryStringParameterAttributeFilter(ISchemaGenerator schemaGenerator) 14 | { 15 | this.schemaGenerator = schemaGenerator ?? throw new ArgumentNullException(nameof(schemaGenerator)); 16 | } 17 | 18 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 19 | { 20 | if (context.MethodInfo.DeclaringType != null) 21 | { 22 | var attributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true) 23 | .Union(context.MethodInfo.GetCustomAttributes(true)) 24 | .OfType(); 25 | 26 | foreach (var attribute in attributes) 27 | { 28 | var apiParameter = new OpenApiParameter 29 | { 30 | Name = attribute.Name, 31 | Description = attribute.Description, 32 | In = ParameterLocation.Query, 33 | Required = attribute.Required, 34 | Schema = this.schemaGenerator.GenerateSchema(attribute?.DataType, new SchemaRepository()) 35 | }; 36 | 37 | if (attribute != null) 38 | { 39 | switch (attribute.Example) 40 | { 41 | case OpenApiNull _: 42 | /* ignore */ 43 | break; 44 | 45 | default: 46 | // set both examples 47 | apiParameter.Schema.Example = attribute.Example; 48 | apiParameter.Example = apiParameter.Schema.Example; 49 | break; 50 | } 51 | } 52 | 53 | operation.Parameters.Add(apiParameter); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckleStartupExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization.Metadata; 4 | using AzureFunctions.Extensions.Swashbuckle.Settings; 5 | using AzureFunctions.Extensions.Swashbuckle.SwashBuckle; 6 | using AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Providers; 7 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 8 | using Microsoft.AspNetCore.Mvc.Formatters; 9 | using Microsoft.AspNetCore.Mvc.ModelBinding; 10 | using Microsoft.Extensions.DependencyInjection; 11 | 12 | namespace AzureFunctions.Extensions.Swashbuckle 13 | { 14 | public static class SwashBuckleStartupExtension 15 | { 16 | public static IServiceCollection AddSwashBuckle( 17 | this IServiceCollection services, 18 | Action? configureDocOptionsAction = null, 19 | Assembly? executingAssembly = null) 20 | { 21 | if (configureDocOptionsAction != null) 22 | { 23 | services.Configure(configureDocOptionsAction); 24 | } 25 | 26 | Assembly? assembly; 27 | 28 | if (executingAssembly == null) 29 | { 30 | assembly = Assembly.GetEntryAssembly(); 31 | 32 | if (assembly == null) 33 | { 34 | throw new ArgumentNullException(nameof(assembly)); 35 | } 36 | } 37 | else 38 | { 39 | assembly = executingAssembly; 40 | } 41 | 42 | services.AddSingleton(); 43 | services.AddSingleton(); 44 | services.AddSingleton(new EmptyModelMetadataProvider()); 45 | services.AddSingleton(new SwashBuckleStartupConfig 46 | { 47 | Assembly = assembly 48 | }); 49 | 50 | var jsonOptions = new JsonSerializerOptions 51 | { 52 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 53 | WriteIndented = true, 54 | TypeInfoResolver = new DefaultJsonTypeInfoResolver() 55 | }; 56 | 57 | var formatter = new SystemTextJsonOutputFormatter(jsonOptions); 58 | services.AddSingleton(formatter); 59 | services.AddSingleton(); 60 | 61 | return services; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/XmlCommentsSchemaFilterChanged.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Xml.XPath; 4 | using AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters.Mapper; 5 | using Microsoft.OpenApi.Models; 6 | using Swashbuckle.AspNetCore.SwaggerGen; 7 | 8 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters 9 | { 10 | public class XmlCommentsSchemaFilterChanged : ISchemaFilter 11 | { 12 | private readonly XPathNavigator xmlNavigator; 13 | 14 | public XmlCommentsSchemaFilterChanged(XPathDocument xmlDoc) 15 | { 16 | this.xmlNavigator = xmlDoc.CreateNavigator() ?? throw new ArgumentException(); 17 | } 18 | 19 | public void Apply(OpenApiSchema schema, SchemaFilterContext context) 20 | { 21 | this.ApplyTypeTags(schema, context.Type); 22 | 23 | if (context.MemberInfo != null && context.ParameterInfo == null) 24 | { 25 | this.ApplyFieldOrPropertyTags(schema, context.MemberInfo); 26 | } 27 | } 28 | 29 | private void ApplyTypeTags(OpenApiSchema schema, Type type) 30 | { 31 | var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(type); 32 | var typeSummaryNode = this.xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{typeMemberName}']/summary"); 33 | 34 | if (typeSummaryNode != null) 35 | { 36 | schema.Description = XmlCommentsTextHelper.Humanize(typeSummaryNode.InnerXml); 37 | } 38 | } 39 | 40 | private void ApplyFieldOrPropertyTags(OpenApiSchema schema, MemberInfo fieldOrPropertyInfo) 41 | { 42 | var fieldOrPropertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(fieldOrPropertyInfo); 43 | var fieldOrPropertyNode = this.xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{fieldOrPropertyMemberName}']"); 44 | 45 | if (fieldOrPropertyNode == null) 46 | { 47 | return; 48 | } 49 | 50 | var summaryNode = fieldOrPropertyNode.SelectSingleNode("summary"); 51 | if (summaryNode != null) 52 | { 53 | schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); 54 | } 55 | 56 | var exampleNode = fieldOrPropertyNode.SelectSingleNode("example"); 57 | if (exampleNode != null) 58 | { 59 | schema.Example = JsonMapper.CreateFromJson(exampleNode.InnerXml); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/SwaggerController.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using AzureFunctions.Extensions.Swashbuckle; 4 | using AzureFunctions.Extensions.Swashbuckle.Attribute; 5 | using Microsoft.Azure.Functions.Worker; 6 | using Microsoft.Azure.Functions.Worker.Http; 7 | 8 | namespace TestFunction 9 | { 10 | public class SwaggerController 11 | { 12 | private readonly ISwashBuckleClient swashBuckleClient; 13 | 14 | public SwaggerController(ISwashBuckleClient swashBuckleClient) 15 | { 16 | this.swashBuckleClient = swashBuckleClient; 17 | } 18 | 19 | [SwaggerIgnore] 20 | [Function("SwaggerJson")] 21 | public async Task SwaggerJson( 22 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/json")] 23 | HttpRequestData req) 24 | { 25 | return await this.swashBuckleClient.CreateSwaggerJsonDocumentResponse(req); 26 | } 27 | 28 | [SwaggerIgnore] 29 | [Function("SwaggerYaml")] 30 | public async Task SwaggerYaml( 31 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/yaml")] 32 | HttpRequestData req) 33 | { 34 | return await this.swashBuckleClient.CreateSwaggerYamlDocumentResponse(req); 35 | } 36 | 37 | [SwaggerIgnore] 38 | [Function("SwaggerUi")] 39 | public async Task SwaggerUi( 40 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/ui")] 41 | HttpRequestData req) 42 | { 43 | return await this.swashBuckleClient.CreateSwaggerUIResponse(req, "swagger/json"); 44 | } 45 | 46 | /// 47 | /// This is only needed for OAuth2 client. This redirecting document is normally served 48 | /// as a static content. Functions don't provide this out of the box, so we serve it here. 49 | /// Don't forget to set OAuth2RedirectPath configuration option to reflect this route. 50 | /// 51 | /// 52 | /// 53 | /// 54 | [SwaggerIgnore] 55 | [Function("SwaggerOAuth2Redirect")] 56 | public async Task SwaggerOAuth2Redirect( 57 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "swagger/oauth2-redirect")] 58 | HttpRequestData req) 59 | { 60 | return await this.swashBuckleClient.CreateSwaggerOAuth2RedirectResponse(req); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/Mapper/JsonMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.OpenApi.Any; 3 | 4 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters.Mapper 5 | { 6 | public static class JsonMapper 7 | { 8 | public static IOpenApiAny? CreateFromJson(string str) 9 | { 10 | try 11 | { 12 | var json = JsonSerializer.Serialize(str); 13 | var jsonElement = JsonSerializer.Deserialize(json); 14 | 15 | if (jsonElement.ValueKind == JsonValueKind.True || jsonElement.ValueKind == JsonValueKind.False) 16 | { 17 | return new OpenApiBoolean(jsonElement.GetBoolean()); 18 | } 19 | 20 | if (jsonElement.ValueKind == JsonValueKind.Number) 21 | { 22 | if (jsonElement.TryGetInt32(out var intValue)) 23 | { 24 | return new OpenApiInteger(intValue); 25 | } 26 | 27 | if (jsonElement.TryGetInt64(out var longValue)) 28 | { 29 | return new OpenApiLong(longValue); 30 | } 31 | 32 | if (jsonElement.TryGetSingle(out var floatValue) && !float.IsInfinity(floatValue)) 33 | { 34 | return new OpenApiFloat(floatValue); 35 | } 36 | 37 | if (jsonElement.TryGetDouble(out var doubleValue)) 38 | { 39 | return new OpenApiDouble(doubleValue); 40 | } 41 | } 42 | 43 | if (jsonElement.ValueKind == JsonValueKind.String) 44 | { 45 | return new OpenApiString(jsonElement.ToString()); 46 | } 47 | 48 | if (jsonElement.ValueKind == JsonValueKind.Null) 49 | { 50 | return new OpenApiNull(); 51 | } 52 | 53 | if (jsonElement.ValueKind == JsonValueKind.Array) 54 | { 55 | return CreateOpenApiArray(jsonElement.EnumerateArray()); 56 | } 57 | } 58 | catch 59 | { 60 | } 61 | 62 | return null; 63 | } 64 | 65 | private static IOpenApiAny CreateOpenApiArray(IEnumerable jsonElements) 66 | { 67 | var openApiArray = new OpenApiArray(); 68 | 69 | foreach (var jsonElement in jsonElements) 70 | { 71 | var json = jsonElement.ValueKind == JsonValueKind.String 72 | ? $"\"{jsonElement}\"" 73 | : jsonElement.ToString(); 74 | 75 | openApiArray.Add(CreateFromJson(json)); 76 | } 77 | 78 | return openApiArray; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/Attribute/QueryStringParameterAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Any; 2 | using System; 3 | 4 | namespace AzureFunctions.Extensions.Swashbuckle.Attribute 5 | { 6 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] 7 | public class QueryStringParameterAttribute : System.Attribute 8 | { 9 | public QueryStringParameterAttribute(string name, string description) 10 | { 11 | this.Initialize(name, description); 12 | this.Example = new OpenApiNull(); 13 | } 14 | 15 | public QueryStringParameterAttribute(string name, string description, string example) 16 | { 17 | this.Initialize(name, description); 18 | this.DataType = typeof(string); 19 | this.Example = new OpenApiString(example); 20 | } 21 | 22 | public QueryStringParameterAttribute(string name, string description, int example) 23 | { 24 | this.Initialize(name, description); 25 | this.DataType = typeof(int); 26 | this.Example = new OpenApiInteger(example); 27 | } 28 | 29 | public QueryStringParameterAttribute(string name, string description, long example) 30 | { 31 | this.Initialize(name, description); 32 | this.DataType = typeof(long); 33 | this.Example = new OpenApiLong(example); 34 | } 35 | 36 | public QueryStringParameterAttribute(string name, string description, double example) 37 | { 38 | this.Initialize(name, description); 39 | this.Example = new OpenApiDouble(example); 40 | } 41 | 42 | public QueryStringParameterAttribute(string name, string description, float example) 43 | { 44 | this.Initialize(name, description); 45 | this.DataType = typeof(float); 46 | this.Example = new OpenApiFloat(example); 47 | } 48 | 49 | public QueryStringParameterAttribute(string name, string description, byte example) 50 | { 51 | this.Initialize(name, description); 52 | this.DataType = typeof(byte); 53 | this.Example = new OpenApiByte(example); 54 | } 55 | 56 | public QueryStringParameterAttribute(string name, string description, bool example) 57 | { 58 | this.Initialize(name, description); 59 | this.DataType = typeof(bool); 60 | this.Example = new OpenApiBoolean(example); 61 | } 62 | 63 | private void Initialize(string name, string description) 64 | { 65 | this.Name = name; 66 | this.Description = description; 67 | } 68 | 69 | public string Name { get; set; } 70 | public Type DataType { get; set; } 71 | public string Description { get; set; } 72 | public bool Required { get; set; } = false; 73 | public IOpenApiAny Example { get; } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/FileUploadOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using AzureFunctions.Extensions.Swashbuckle.Attribute; 4 | using Microsoft.OpenApi.Any; 5 | using Microsoft.OpenApi.Models; 6 | using Swashbuckle.AspNetCore.SwaggerGen; 7 | 8 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters 9 | { 10 | public class FileUploadOperationFilter : IOperationFilter 11 | { 12 | private const string MimeType = "multipart/form-data"; 13 | 14 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 15 | { 16 | if (context.MethodInfo.DeclaringType != null) 17 | { 18 | var uploadFiles = context.MethodInfo.DeclaringType.GetCustomAttributes(true) 19 | .Union(context.MethodInfo.GetCustomAttributes(true)) 20 | .OfType(); 21 | 22 | var swaggerUploadFiles = uploadFiles.ToList(); 23 | 24 | if (!swaggerUploadFiles.Any()) 25 | { 26 | return; 27 | } 28 | 29 | var uploadFile = swaggerUploadFiles.First(); 30 | operation.RequestBody ??= new OpenApiRequestBody(); 31 | 32 | if (!operation.RequestBody.Content.ContainsKey(MimeType)) 33 | { 34 | operation.RequestBody.Content[MimeType] = new OpenApiMediaType(); 35 | } 36 | 37 | operation.RequestBody.Content[MimeType].Schema ??= new OpenApiSchema(); 38 | 39 | var uploadFileName = string.IsNullOrEmpty(uploadFile.Name) 40 | ? "uploadedFile" 41 | : uploadFile.Name; 42 | 43 | var uploadFileDescription = string.IsNullOrEmpty(uploadFile.Description) 44 | ? "File to upload." 45 | : uploadFile.Description; 46 | 47 | var uploadFileMediaType = new OpenApiMediaType 48 | { 49 | Schema = new OpenApiSchema 50 | { 51 | Type = "object", 52 | Properties = 53 | { 54 | [uploadFileName] = new OpenApiSchema 55 | { 56 | Description = uploadFileDescription, 57 | Type = "file", 58 | Format = "binary" 59 | } 60 | }, 61 | Required = new HashSet 62 | { 63 | uploadFileName 64 | } 65 | } 66 | }; 67 | 68 | operation.RequestBody = new OpenApiRequestBody 69 | { 70 | Content = 71 | { 72 | [MimeType] = uploadFileMediaType 73 | } 74 | }; 75 | 76 | if (!string.IsNullOrEmpty(uploadFile.Example)) 77 | { 78 | operation.RequestBody.Content[MimeType].Schema.Example = 79 | new OpenApiString(uploadFile.Example); 80 | operation.RequestBody.Content[MimeType].Schema.Description = uploadFile.Example; 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/XmlCommentsParameterFilterWithExamples.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Xml.XPath; 4 | using AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters.Mapper; 5 | using Microsoft.OpenApi.Models; 6 | using Swashbuckle.AspNetCore.SwaggerGen; 7 | 8 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters 9 | { 10 | public class XmlCommentsParameterFilterWithExamples : IParameterFilter 11 | { 12 | private readonly XPathNavigator xmlNavigator; 13 | 14 | public XmlCommentsParameterFilterWithExamples(XPathDocument xmlDoc) 15 | { 16 | this.xmlNavigator = xmlDoc.CreateNavigator() ?? throw new ArgumentException(); 17 | } 18 | 19 | public void Apply(OpenApiParameter parameter, ParameterFilterContext context) 20 | { 21 | if (context.PropertyInfo != null) 22 | { 23 | this.ApplyPropertyTags(parameter, context.PropertyInfo); 24 | } 25 | else if (context.ParameterInfo != null) 26 | { 27 | this.ApplyParamTags(parameter, context.ParameterInfo); 28 | } 29 | } 30 | 31 | private void ApplyPropertyTags(OpenApiParameter parameter, PropertyInfo propertyInfo) 32 | { 33 | var propertyMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(propertyInfo); 34 | var propertyNode = this.xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{propertyMemberName}']"); 35 | 36 | if (propertyNode == null) 37 | { 38 | return; 39 | } 40 | 41 | var summaryNode = propertyNode.SelectSingleNode("summary"); 42 | if (summaryNode != null) 43 | { 44 | parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); 45 | } 46 | 47 | var exampleNode = propertyNode.SelectSingleNode("example"); 48 | if (exampleNode != null) 49 | { 50 | parameter.Example = JsonMapper.CreateFromJson(exampleNode.InnerXml); 51 | } 52 | } 53 | 54 | private void ApplyParamTags(OpenApiParameter parameter, ParameterInfo parameterInfo) 55 | { 56 | if (!(parameterInfo.Member is MethodInfo methodInfo)) 57 | { 58 | return; 59 | } 60 | 61 | // If method is from a constructed generic type, look for comments from the generic type method 62 | var targetMethod = methodInfo.DeclaringType!.IsConstructedGenericType 63 | ? methodInfo.GetUnderlyingGenericTypeMethod() 64 | : methodInfo; 65 | 66 | if (targetMethod == null) 67 | { 68 | return; 69 | } 70 | 71 | var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(targetMethod); 72 | var paramNode = this.xmlNavigator.SelectSingleNode( 73 | $"/doc/members/member[@name='{methodMemberName}']/param[@name='{parameterInfo.Name}']"); 74 | 75 | if (paramNode != null) 76 | { 77 | parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml); 78 | 79 | var example = paramNode.GetAttribute("example", string.Empty); 80 | if (!string.IsNullOrEmpty(example)) 81 | { 82 | parameter.Example = JsonMapper.CreateFromJson(example); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Hosting; 4 | using System.Reflection; 5 | using AzureFunctions.Extensions.Swashbuckle.Settings; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.OpenApi; 8 | using Microsoft.OpenApi.Models; 9 | using AzureFunctions.Extensions.Swashbuckle; 10 | using Swashbuckle.AspNetCore.SwaggerGen; 11 | 12 | var host = new HostBuilder() 13 | .ConfigureFunctionsWebApplication(builder => 14 | { 15 | }) 16 | .ConfigureAppConfiguration((hostContext, services) => 17 | { 18 | 19 | }) 20 | .ConfigureServices((hostContext, services) => 21 | { 22 | //Register the extension 23 | services.AddSwashBuckle(opts => 24 | { 25 | // If you want to add Newtonsoft support insert next line 26 | // opts.AddNewtonsoftSupport = true; 27 | opts.RoutePrefix = "api"; 28 | opts.SpecVersion = OpenApiSpecVersion.OpenApi3_0; 29 | opts.AddCodeParameter = true; 30 | opts.PrependOperationWithRoutePrefix = true; 31 | opts.XmlPath = "TestFunction.xml"; 32 | opts.Documents = new[] 33 | { 34 | new SwaggerDocument 35 | { 36 | Name = "v1", 37 | Title = "Swagger document", 38 | Description = "Swagger test document", 39 | Version = "v2" 40 | }, 41 | new SwaggerDocument 42 | { 43 | Name = "v2", 44 | Title = "Swagger document 2", 45 | Description = "Swagger test document 2", 46 | Version = "v2" 47 | } 48 | }; 49 | opts.Title = "Swagger Test"; 50 | //opts.OverridenPathToSwaggerJson = new Uri("http://localhost:7071/api/Swagger/json"); 51 | opts.ConfigureSwaggerGen = x => 52 | { 53 | //custom operation example 54 | x.CustomOperationIds(apiDesc => apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) 55 | ? methodInfo.Name 56 | : new Guid().ToString()); 57 | 58 | //custom filter example 59 | //x.DocumentFilter(); 60 | 61 | //oauth2 62 | x.AddSecurityDefinition("oauth2", 63 | new OpenApiSecurityScheme 64 | { 65 | Type = SecuritySchemeType.OAuth2, 66 | Flows = new OpenApiOAuthFlows 67 | { 68 | Implicit = new OpenApiOAuthFlow 69 | { 70 | AuthorizationUrl = new Uri("https://your.idserver.net/connect/authorize"), 71 | Scopes = new Dictionary 72 | { 73 | { "api.read", "Access read operations" }, 74 | { "api.write", "Access write operations" } 75 | } 76 | } 77 | } 78 | }); 79 | }; 80 | 81 | // set up your client ID if your API is protected 82 | opts.ClientId = "your.client.id"; 83 | opts.ClientSecret = "your.client.secret"; 84 | opts.UsePkceWithAuthorizationCodeGrant = true; 85 | opts.OAuth2RedirectPath = "http://localhost:7071/api/swagger/oauth2-redirect"; 86 | }); 87 | }) 88 | .Build(); 89 | 90 | host.Run(); 91 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckleClientExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Worker.Http; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace AzureFunctions.Extensions.Swashbuckle 10 | { 11 | public static class SwashBuckleClientExtension 12 | { 13 | public static async Task CreateSwaggerJsonDocumentResponse( 14 | this ISwashBuckleClient client, 15 | HttpRequestData requestData, 16 | string documentName = "v1") 17 | { 18 | var host = GetBaseUri(requestData); 19 | 20 | var stream = client.GetSwaggerJsonDocument(host, documentName); 21 | var reader = new StreamReader(stream); 22 | var document = await reader.ReadToEndAsync(); 23 | 24 | var response = requestData.CreateResponse(HttpStatusCode.OK); 25 | await response.WriteStringAsync(document, CancellationToken.None, Encoding.UTF8); 26 | 27 | //response.Headers.Add("Content-Type", "application/json; charset=utf-8"); 28 | return response; 29 | } 30 | 31 | public static async Task CreateSwaggerYamlDocumentResponse( 32 | this ISwashBuckleClient client, 33 | HttpRequestData requestData, 34 | string documentName = "v1") 35 | { 36 | var host = GetBaseUri(requestData); 37 | 38 | var stream = client.GetSwaggerYamlDocument(host, documentName); 39 | var reader = new StreamReader(stream); 40 | 41 | var response = requestData.CreateResponse(HttpStatusCode.OK); 42 | 43 | var document = await reader.ReadToEndAsync(); 44 | await response.WriteStringAsync(document, CancellationToken.None, Encoding.UTF8); 45 | response.Headers.Add("Content-Type", "application/json; charset=utf-8"); 46 | 47 | return response; 48 | } 49 | 50 | public static async Task CreateSwaggerUIResponse( 51 | this ISwashBuckleClient client, 52 | HttpRequestData requestData, 53 | string documentRoute) 54 | { 55 | var routePrefix = string.IsNullOrEmpty(client.RoutePrefix) 56 | ? string.Empty 57 | : $"/{client.RoutePrefix}"; 58 | 59 | var host = GetBaseUri(requestData); 60 | var stream = client.GetSwaggerUi($"{host}{routePrefix}/{documentRoute}"); 61 | 62 | var response = requestData.CreateResponse(HttpStatusCode.OK); 63 | using var reader = new StreamReader(stream); 64 | var document = await reader.ReadToEndAsync(); 65 | await response.WriteStringAsync(document, CancellationToken.None, Encoding.UTF8); 66 | //response.Headers.Add("Content-Type", "text/html; charset=utf-8"); 67 | return response; 68 | } 69 | 70 | public static async Task CreateSwaggerOAuth2RedirectResponse( 71 | this ISwashBuckleClient client, 72 | HttpRequestData requestData) 73 | { 74 | var stream = client.GetSwaggerOAuth2Redirect(); 75 | 76 | var response = requestData.CreateResponse(HttpStatusCode.OK); 77 | using var reader = new StreamReader(stream); 78 | var document = await reader.ReadToEndAsync(); 79 | await response.WriteStringAsync(document, CancellationToken.None, Encoding.UTF8); 80 | //response.Headers.Add("Content-Type", "text/html; charset=utf-8"); 81 | return response; 82 | } 83 | 84 | private static string GetBaseUri(HttpRequestData requestData) 85 | { 86 | var host = requestData.Url.Host; 87 | var port = requestData.Url.Port; // Get the port 88 | var scheme = requestData.Url.Scheme; 89 | 90 | var hostWithPort = (port == 80 && scheme == "http") || (port == 443 && scheme == "https") ? host : $"{host}:{port}"; 91 | 92 | var fullHost = $"{scheme}://{hostWithPort}"; 93 | return fullHost; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | AzureExtensions.Swashbuckle 5 | Vitaly Bibikov 6 | Swagger and Swagger UI in Azure Functions by Swashbuckle 7 | AzureFunctions.Extensions.Swashbuckle 8 | 4.0.4 9 | AzureFunctions.Extensions.Swashbuckle 10 | vitalybibikov 11 | https://github.com/vitalybibikov/azure-functions-extensions-swashbuckle 12 | https://github.com/vitalybibikov/azure-functions-extensions-swashbuckle 13 | GitHub 14 | Swagger Swashbuckle Azure Functions openapi openapi3 isolated worker 15 | false 16 | Open API 17 | true 18 | enable 19 | enable 20 | true 21 | True 22 | $(NoWarn);CS1591 23 | 24 | 25 | 26 | true 27 | snupkg 28 | true 29 | true 30 | 31 | 32 | 33 | true 34 | 35 | 36 | 37 | true 38 | true 39 | ..\..\..\StyleCop.ruleset 40 | 41 | 42 | 43 | 44 | all 45 | 46 | 47 | all 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | all 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | PreserveNewest 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | all 83 | 84 | 85 | 86 | 87 | 88 | 89 | True 90 | 91 | 92 | 93 | 94 | 95 | LICENSE 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/Mapper/TypeMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.OpenApi.Models; 4 | 5 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters.Mapper 6 | { 7 | public static class TypeMapper 8 | { 9 | private static readonly Dictionary> SimpleTypeToOpenApiSchema = 10 | new Dictionary> 11 | { 12 | [typeof(bool)] = () => new OpenApiSchema { Type = "boolean" }, 13 | [typeof(byte)] = () => new OpenApiSchema { Type = "string", Format = "byte" }, 14 | [typeof(int)] = () => new OpenApiSchema { Type = "integer", Format = "int32" }, 15 | [typeof(uint)] = () => new OpenApiSchema { Type = "integer", Format = "int32" }, 16 | [typeof(ushort)] = () => new OpenApiSchema { Type = "integer", Format = "int32" }, 17 | [typeof(long)] = () => new OpenApiSchema { Type = "integer", Format = "int64" }, 18 | [typeof(ulong)] = () => new OpenApiSchema { Type = "integer", Format = "int64" }, 19 | [typeof(float)] = () => new OpenApiSchema { Type = "number", Format = "float" }, 20 | [typeof(double)] = () => new OpenApiSchema { Type = "number", Format = "double" }, 21 | [typeof(decimal)] = () => new OpenApiSchema { Type = "number", Format = "double" }, 22 | [typeof(DateTime)] = () => new OpenApiSchema { Type = "string", Format = "date-time" }, 23 | [typeof(DateTimeOffset)] = () => new OpenApiSchema { Type = "string", Format = "date-time" }, 24 | [typeof(Guid)] = () => new OpenApiSchema { Type = "string", Format = "uuid" }, 25 | [typeof(char)] = () => new OpenApiSchema { Type = "string" }, 26 | 27 | [typeof(bool?)] = () => new OpenApiSchema { Type = "boolean", Nullable = true }, 28 | [typeof(byte?)] = () => new OpenApiSchema { Type = "string", Format = "byte", Nullable = true }, 29 | [typeof(int?)] = () => new OpenApiSchema { Type = "integer", Format = "int32", Nullable = true }, 30 | [typeof(uint?)] = () => new OpenApiSchema { Type = "integer", Format = "int32", Nullable = true }, 31 | [typeof(ushort?)] = () => new OpenApiSchema { Type = "integer", Format = "int32", Nullable = true }, 32 | [typeof(long?)] = () => new OpenApiSchema { Type = "integer", Format = "int64", Nullable = true }, 33 | [typeof(ulong?)] = () => new OpenApiSchema { Type = "integer", Format = "int64", Nullable = true }, 34 | [typeof(float?)] = () => new OpenApiSchema { Type = "number", Format = "float", Nullable = true }, 35 | [typeof(double?)] = () => new OpenApiSchema { Type = "number", Format = "double", Nullable = true }, 36 | [typeof(decimal?)] = () => new OpenApiSchema { Type = "number", Format = "double", Nullable = true }, 37 | [typeof(DateTime?)] = () => new OpenApiSchema { Type = "string", Format = "date-time", Nullable = true }, 38 | [typeof(DateTimeOffset?)] = () => 39 | new OpenApiSchema { Type = "string", Format = "date-time", Nullable = true }, 40 | [typeof(Guid?)] = () => new OpenApiSchema { Type = "string", Format = "uuid", Nullable = true }, 41 | [typeof(char?)] = () => new OpenApiSchema { Type = "string", Nullable = true }, 42 | 43 | // Uri is treated as simple string. 44 | [typeof(Uri)] = () => new OpenApiSchema { Type = "string" }, 45 | 46 | [typeof(string)] = () => new OpenApiSchema { Type = "string" }, 47 | 48 | [typeof(object)] = () => new OpenApiSchema { Type = "object" } 49 | }; 50 | 51 | public static OpenApiSchema ToOpenApiSpecType(this Type type) 52 | { 53 | if (type == null) 54 | { 55 | throw new ArgumentNullException(nameof(type)); 56 | } 57 | 58 | return SimpleTypeToOpenApiSchema.TryGetValue(type, out var result) 59 | ? result() 60 | : new OpenApiSchema { Type = "string" }; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Filters/XmlCommentsOperationFilterWithParams.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Xml.XPath; 3 | using Microsoft.OpenApi.Models; 4 | using Swashbuckle.AspNetCore.SwaggerGen; 5 | 6 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters 7 | { 8 | public class XmlCommentsOperationFilterWithParams : IOperationFilter 9 | { 10 | private readonly XPathNavigator xmlNavigator; 11 | 12 | public XmlCommentsOperationFilterWithParams(XPathDocument xmlDoc) 13 | { 14 | this.xmlNavigator = xmlDoc.CreateNavigator() ?? throw new ArgumentException(); 15 | } 16 | 17 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 18 | { 19 | if (context.MethodInfo == null) 20 | { 21 | return; 22 | } 23 | 24 | var targetMethod = context.MethodInfo.DeclaringType!.IsConstructedGenericType 25 | ? context.MethodInfo.GetUnderlyingGenericTypeMethod() 26 | : context.MethodInfo; 27 | 28 | if (targetMethod == null) 29 | { 30 | return; 31 | } 32 | 33 | this.ApplyParameters(operation, targetMethod); 34 | this.ApplyControllerTags(operation, targetMethod.DeclaringType!); 35 | this.ApplyMethodTags(operation, targetMethod); 36 | } 37 | 38 | private void ApplyParameters(OpenApiOperation operation, MethodInfo methodInfo) 39 | { 40 | if (methodInfo != null) 41 | { 42 | var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo); 43 | foreach (var parameter in methodInfo.GetParameters()) 44 | { 45 | if (!string.IsNullOrEmpty(parameter.Name)) 46 | { 47 | var paramNode = this.xmlNavigator.SelectSingleNode( 48 | $"/doc/members/member[@name='{methodMemberName}']/param[@name='{parameter.Name}']"); 49 | 50 | if (paramNode != null) 51 | { 52 | var humanizedDescription = XmlCommentsTextHelper.Humanize(paramNode.InnerXml); 53 | 54 | var operationParameter = operation.Parameters 55 | .FirstOrDefault(x => x.Name == parameter.Name); 56 | 57 | if (operationParameter != null) 58 | { 59 | operationParameter.Description = humanizedDescription; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | private void ApplyControllerTags(OpenApiOperation operation, Type controllerType) 68 | { 69 | var typeMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(controllerType); 70 | var responseNodes = this.xmlNavigator.Select($"/doc/members/member[@name='{typeMemberName}']/response"); 71 | this.ApplyResponseTags(operation, responseNodes); 72 | } 73 | 74 | private void ApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo) 75 | { 76 | var methodMemberName = XmlCommentsNodeNameHelper.GetMemberNameForMethod(methodInfo); 77 | var methodNode = this.xmlNavigator.SelectSingleNode($"/doc/members/member[@name='{methodMemberName}']"); 78 | 79 | if (methodNode == null) 80 | { 81 | return; 82 | } 83 | 84 | var summaryNode = methodNode.SelectSingleNode("summary"); 85 | if (summaryNode != null) 86 | { 87 | operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); 88 | } 89 | 90 | var remarksNode = methodNode.SelectSingleNode("remarks"); 91 | if (remarksNode != null) 92 | { 93 | operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml); 94 | } 95 | 96 | var responseNodes = methodNode.Select("response"); 97 | this.ApplyResponseTags(operation, responseNodes); 98 | } 99 | 100 | private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator responseNodes) 101 | { 102 | while (responseNodes.MoveNext()) 103 | { 104 | var code = responseNodes.Current!.GetAttribute("code", string.Empty); 105 | var response = operation.Responses.TryGetValue(code, out var operationResponse) 106 | ? operationResponse 107 | : operation.Responses[code] = new OpenApiResponse(); 108 | 109 | response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | nupkgs/ 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/TestFunction.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TestFunction 5 | 6 | 7 | 8 | 9 | Some model 10 | 11 | 12 | 13 | 14 | Id (example) 15 | 16 | 3 17 | 18 | 19 | 20 | Name (example) 21 | 22 | John 23 | 24 | 25 | 26 | Description (example) 27 | 28 | Sometimes human 29 | 30 | 31 | 32 | This is only needed for OAuth2 client. This redirecting document is normally served 33 | as a static content. Functions don't provide this out of the box, so we serve it here. 34 | Don't forget to set OAuth2RedirectPath configuration option to reflect this route. 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | TestGet Function 43 | 44 | some request 45 | some id 46 | Awesomeness! 47 | Product created 48 | 49 | 50 | 51 | TestGet Function 52 | 53 | some request 54 | some id 55 | Awesomeness! 56 | Product created 57 | 58 | 59 | 60 | 61 | 62 | 63 | Extension methods to enable registration of the custom implementation generated for the current worker. 64 | 65 | 66 | 67 | 68 | Configures an optimized function executor to the invocation pipeline. 69 | 70 | 71 | 72 | 73 | Auto startup class to register the custom implementation generated for the current worker. 74 | 75 | 76 | 77 | 78 | Configures the to use the custom implementation generated for the current worker. 79 | 80 | The instance to use for service registration. 81 | 82 | 83 | 84 | Custom implementation that returns function metadata definitions for the current worker."/> 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Extension methods to enable registration of the custom implementation generated for the current worker. 93 | 94 | 95 | 96 | 97 | Adds the GeneratedFunctionMetadataProvider to the service collection. 98 | During initialization, the worker will return generated function metadata instead of relying on the Azure Functions host for function indexing. 99 | 100 | 101 | 102 | 103 | Auto startup class to register the custom implementation generated for the current worker. 104 | 105 | 106 | 107 | 108 | Configures the to use the custom implementation generated for the current worker. 109 | 110 | The instance to use for service registration. 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/TestFunction/TestController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | using AzureFunctions.Extensions.Swashbuckle.Attribute; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Azure.Functions.Worker; 13 | using TestFunction.Models; 14 | 15 | namespace TestFunction 16 | { 17 | [ApiExplorerSettings(GroupName = "testee")] 18 | public class TestController 19 | { 20 | [ProducesResponseType(typeof(TestModel[]), (int)HttpStatusCode.OK)] 21 | [Function("TestGetWithExamples")] 22 | [QueryStringParameter("colour", "The colour of the bike", "Red", Required = true)] 23 | [QueryStringParameter("wheelsize", "Size of wheel", 26, Required = true)] 24 | [QueryStringParameter("used", "Must be used", false, Required = false)] 25 | public async Task GetExamples([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "getexampletest")] 26 | HttpRequest request) 27 | { 28 | return new OkObjectResult(new[] { new TestModel(), new TestModel() }); 29 | } 30 | 31 | 32 | [ProducesResponseType(typeof(TestModel[]), (int)HttpStatusCode.OK)] 33 | [Function("TestGets")] 34 | [QueryStringParameter("expand", "it is expand parameter", DataType = typeof(int), Required = true)] 35 | public async Task Gets([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "test")] 36 | HttpRequest request) 37 | { 38 | return new OkObjectResult(new[] { new TestModel(), new TestModel() }); 39 | } 40 | 41 | /// 42 | /// TestGet Function 43 | /// 44 | /// some request 45 | /// some id 46 | /// Awesomeness! 47 | /// Product created 48 | [ProducesResponseType(typeof(TestModel), (int)HttpStatusCode.OK)] 49 | [Function("TestGet")] 50 | [ApiExplorerSettings(GroupName = "v1")] 51 | public Task Get( 52 | int id, 53 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "v1/test/{id}")] 54 | HttpRequest request) 55 | { 56 | return Task.FromResult(new OkObjectResult(new TestModel())); 57 | } 58 | 59 | /// 60 | /// TestGet Function 61 | /// 62 | /// some request 63 | /// some id 64 | /// Awesomeness! 65 | /// Product created 66 | [ProducesResponseType(typeof(TestModel), (int)HttpStatusCode.OK)] 67 | [Function("TestGetv2")] 68 | [ApiExplorerSettings(GroupName = "v2")] 69 | public Task Get2( 70 | int id, 71 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "v2/test/{id}")] 72 | HttpRequest request) 73 | { 74 | return Task.FromResult(new OkObjectResult(new TestModel())); 75 | } 76 | 77 | [ProducesResponseType(typeof(TestModel), (int)HttpStatusCode.OK)] 78 | [QueryStringParameter("name", "this is name", DataType = typeof(string), Required = true)] 79 | [Function("TestGetCat")] 80 | public Task GetCat( 81 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "cat/{id}/{testId?}")] 82 | HttpRequest request, int id, int? testId) 83 | { 84 | return Task.FromResult(new OkObjectResult(new TestModel())); 85 | } 86 | 87 | [ProducesResponseType(typeof(TestModel), (int)HttpStatusCode.OK)] 88 | [QueryStringParameter("pageSorting", "pageSorting", DataType = typeof(IEnumerable), Required = false)] 89 | [Function("TestGetSomethingWithArray")] 90 | public Task GetSomethingWithArray( 91 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "arraytest")] 92 | HttpRequest request) 93 | { 94 | var items = request.Query["pageSorting"].ToList(); 95 | return Task.FromResult(new OkObjectResult(items)); 96 | } 97 | 98 | [ProducesResponseType(typeof(TestModel), (int)HttpStatusCode.Created)] 99 | [Function("TestAdd")] 100 | public Task Add([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "test")] 101 | TestModel testModel) 102 | { 103 | return Task.FromResult(new CreatedResult("", testModel)); 104 | } 105 | 106 | [ProducesResponseType(typeof(TestModel), (int)HttpStatusCode.Created)] 107 | [Function("TestRequestBodyTypePresented")] 108 | public async Task RequestBodyTypePresented( 109 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "testandget/{id}")] 110 | [RequestBodyType(typeof(TestModel), "testmodel")] 111 | HttpRequest httpRequest, 112 | long id) 113 | { 114 | if (httpRequest.Method.Equals("post", StringComparison.OrdinalIgnoreCase)) 115 | { 116 | using var reader = new StreamReader(httpRequest.Body); 117 | var json = await reader.ReadToEndAsync(); 118 | var testModel = JsonSerializer.Deserialize(json); 119 | return new CreatedResult("", testModel); 120 | } 121 | 122 | return new OkResult(); 123 | } 124 | 125 | [ProducesResponseType(typeof(TestModel), (int)HttpStatusCode.Created)] 126 | [RequestHttpHeader("x-ms-session-id", true)] 127 | [Function("TestUpload")] 128 | [SwaggerUploadFile("Pdf", "Pdf upload")] 129 | public async Task TestUpload( 130 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "test/upload")] HttpRequest req) 131 | { 132 | var data = await req.ReadFormAsync(); 133 | 134 | if (data != null) 135 | { 136 | foreach (var formFile in data.Files) 137 | { 138 | using var reader = new StreamReader(formFile.OpenReadStream()); 139 | var fileContent = await reader.ReadToEndAsync(); 140 | return new OkObjectResult(fileContent.Length); 141 | } 142 | } 143 | 144 | return new NoContentResult(); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # AzureExtensions.Swashbuckle v4.0.0 3 | 4 | ### (Searching for collaborators!) 5 | 6 | OpenAPI 2/3 implementation based on Swashbuckle(Swagger) tooling for API's built with Azure Functions 7 | 8 | This product aims to easily provide Swagger and Swagger UI of APIs created in Azure Functions using isolated worker model 9 | 10 | ------------------------------ 11 | 4.0.4 12 | - Updated to latest dependencies 13 | 14 | ------------------------------ 15 | 16 | 4.0.3 17 | by @MikeHookTransparity 18 | Added ClientSecret, UseBasicAuthenticationWithAccessCodeGrant 19 | 20 | ------------------------------ 21 | 4.0.2 22 | - Updated to latest Swagger 6.5.1 23 | - Fixes & Improvements 24 | - Added symbols package 25 | 26 | ------------------------------ 27 | 4.0.1 28 | 29 | - Fixed some minor issues 30 | - Updated to new language features 31 | - Added exception handling and nullability 32 | - Doocumenation fixes 33 | 34 | ------------------------------ 35 | 4.0.0-beta 36 | - just remebering what the heck is going om here 37 | - Updated to v4 Functions 38 | - Updated to .NET 8 39 | - Updated to isolated worker model (from now on it is going to be the only one that is supported, as inprocess is going to be deprecated) 40 | - Updated to UI v5.17.3 41 | - Updated to Swagger 5.6.5 42 | - Updated docs 43 | - Considering removing support of NewtonJson 44 | 45 | https://www.nuget.org/packages/AzureExtensions.Swashbuckle/4.0.0-beta 46 | ------------------------------ 47 | 3.3.1-beta 48 | 49 | - #64 Support for authorization configuration 50 | - #60 Consolidated extensions and added one to support .net 5 51 | - Updated docs 52 | - Updated js/html/css libs 53 | - Some classed made public to support 3-party IoC. 54 | - Fixed several issues, related to versioning and XML comments. 55 | - Updated to UI v3.37.2 56 | - Updated to Swagger 5.6.3 57 | - Updated documentation 58 | - Ability to create multiple versions of documents, example added. 59 | - Added examples of a custom filter, improved test application 60 | 61 | https://www.nuget.org/packages/AzureExtensions.Swashbuckle/4.0.0-beta 62 | 63 | 64 | ------------------------------ 65 | 3.1.6 66 | 67 | https://www.nuget.org/packages/AzureExtensions.Swashbuckle/3.1.6 68 | 69 | Fixed #8, #9 70 | 71 | Updated to UI v3.25.1 72 | 73 | Updated to Swagger 5.4.1 74 | 75 | Fixed base url for Swagger UI 76 | 77 | **Breaking:** 78 | 79 | Option and DocumentOption renamed to SwaggerDocOptions and SwaggerDocument respectivly 80 | and moved to AzureFunctions.Extensions.Swashbuckle.Settings namespace 81 | 82 | **Properties renamed:** 83 | 84 | PrepandOperationWithRoutePrefix => PrependOperationWithRoutePrefix 85 | 86 | AddCodeParamater => AddCodeParameter 87 | 88 | **Properties added:** 89 | 90 | Added ability to configure SwaggerGen via ConfigureSwaggerGen 91 | 92 | Added ability to override default url to Swagger json document (in case of reverse proxy/gateway/ingress) are used. 93 | 94 | 95 | **Size:** 96 | 97 | All the resources are places in zip archive in order to decrease result dll size by 338% (from 1.594kb to 472kb) 98 | 99 | 100 | 101 | ------------------------------ 102 | 3.0.0 103 | - Updated to v3 Functions 104 | - Updated to 5.0.0 Swashbuckle.AspNetCore nugets 105 | - Merged PRs to fix issues related to RequestBodyType and Ignore attribute 106 | - application/json is a default media type. 107 | 108 | 109 | 110 | # Sample 111 | 112 | https://github.com/vitalybibikov/azure-functions-extensions-swashbuckle/tree/master/src/AzureFunctions.Extensions.Swashbuckle/TestFunction 113 | 114 | # Update 115 | 116 | Version 3.0.0 117 | 118 | 119 | # Getting Started 120 | 121 | 1. Install the standard Nuget package into your Azure Functions application. 122 | 123 | ``` 124 | Package Manager : Install-Package AzureExtensions.Swashbuckle 125 | CLI : dotnet add package AzureExtensions.Swashbuckle 126 | ``` 127 | 128 | 2. Add Program.cs class on your Functions project. 129 | 130 | !!! Now you need to specify in option the RoutePrefix. 131 | 132 | opts.RoutePrefix = "api"; 133 | 134 | 135 | ```csharp 136 | using System; 137 | using System.Collections.Generic; 138 | using Microsoft.Extensions.Hosting; 139 | using System.Reflection; 140 | using AzureFunctions.Extensions.Swashbuckle.Settings; 141 | using Microsoft.Extensions.DependencyInjection; 142 | using Microsoft.OpenApi; 143 | using Microsoft.OpenApi.Models; 144 | using AzureFunctions.Extensions.Swashbuckle; 145 | using Swashbuckle.AspNetCore.SwaggerGen; 146 | 147 | var host = new HostBuilder() 148 | .ConfigureServices((hostContext, services) => 149 | { 150 | //Register the extension 151 | services.AddSwashBuckle(opts => 152 | { 153 | // If you want to add Newtonsoft support insert next line 154 | // opts.AddNewtonsoftSupport = true; 155 | opts.RoutePrefix = "api"; 156 | opts.SpecVersion = OpenApiSpecVersion.OpenApi3_0; 157 | opts.AddCodeParameter = true; 158 | opts.PrependOperationWithRoutePrefix = true; 159 | opts.XmlPath = "TestFunction.xml"; 160 | opts.Documents = new[] 161 | { 162 | new SwaggerDocument 163 | { 164 | Name = "v1", 165 | Title = "Swagger document", 166 | Description = "Swagger test document", 167 | Version = "v2" 168 | }, 169 | new SwaggerDocument 170 | { 171 | Name = "v2", 172 | Title = "Swagger document 2", 173 | Description = "Swagger test document 2", 174 | Version = "v2" 175 | } 176 | }; 177 | opts.Title = "Swagger Test"; 178 | //opts.OverridenPathToSwaggerJson = new Uri("http://localhost:7071/api/Swagger/json"); 179 | opts.ConfigureSwaggerGen = x => 180 | { 181 | //custom operation example 182 | x.CustomOperationIds(apiDesc => apiDesc.TryGetMethodInfo(out MethodInfo methodInfo) 183 | ? methodInfo.Name 184 | : new Guid().ToString()); 185 | 186 | //custom filter example 187 | //x.DocumentFilter(); 188 | 189 | //oauth2 190 | x.AddSecurityDefinition("oauth2", 191 | new OpenApiSecurityScheme 192 | { 193 | Type = SecuritySchemeType.OAuth2, 194 | Flows = new OpenApiOAuthFlows 195 | { 196 | Implicit = new OpenApiOAuthFlow 197 | { 198 | AuthorizationUrl = new Uri("https://your.idserver.net/connect/authorize"), 199 | Scopes = new Dictionary 200 | { 201 | { "api.read", "Access read operations" }, 202 | { "api.write", "Access write operations" } 203 | } 204 | } 205 | } 206 | }); 207 | }; 208 | 209 | // set up your client ID if your API is protected 210 | opts.ClientId = "your.client.id"; 211 | opts.OAuth2RedirectPath = "http://localhost:7071/api/swagger/oauth2-redirect"; 212 | }); 213 | }) 214 | .Build(); 215 | 216 | host.Run(); 217 | 218 | ``` 219 | 220 | 3. Add swagger and swagger ui endpoint functions on your project. 221 | 222 | ```csharp 223 | public class SwaggerController 224 | { 225 | private readonly ISwashBuckleClient swashBuckleClient; 226 | 227 | public SwaggerController(ISwashBuckleClient swashBuckleClient) 228 | { 229 | this.swashBuckleClient = swashBuckleClient; 230 | } 231 | 232 | [SwaggerIgnore] 233 | [Function("SwaggerJson")] 234 | public async Task SwaggerJson( 235 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/json")] 236 | HttpRequestData req) 237 | { 238 | return await this.swashBuckleClient.CreateSwaggerJsonDocumentResponse(req); 239 | } 240 | 241 | [SwaggerIgnore] 242 | [Function("SwaggerYaml")] 243 | public async Task SwaggerYaml( 244 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/yaml")] 245 | HttpRequestData req) 246 | { 247 | return await this.swashBuckleClient.CreateSwaggerYamlDocumentResponse(req); 248 | } 249 | 250 | [SwaggerIgnore] 251 | [Function("SwaggerUi")] 252 | public async Task SwaggerUi( 253 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "Swagger/ui")] 254 | HttpRequestData req) 255 | { 256 | return await this.swashBuckleClient.CreateSwaggerUIResponse(req, "swagger/json"); 257 | } 258 | 259 | /// 260 | /// This is only needed for OAuth2 client. This redirecting document is normally served 261 | /// as a static content. Functions don't provide this out of the box, so we serve it here. 262 | /// Don't forget to set OAuth2RedirectPath configuration option to reflect this route. 263 | /// 264 | /// 265 | /// 266 | /// 267 | [SwaggerIgnore] 268 | [Function("SwaggerOAuth2Redirect")] 269 | public async Task SwaggerOAuth2Redirect( 270 | [HttpTrigger(AuthorizationLevel.Function, "get", Route = "swagger/oauth2-redirect")] 271 | HttpRequestData req) 272 | { 273 | return await this.swashBuckleClient.CreateSwaggerOAuth2RedirectResponse(req); 274 | } 275 | } 276 | ``` 277 | 278 | 4. Open Swagger UI URL in your browser. 279 | 280 | If you does not changed api route prefix. Swagger UI URL is https://hostname/api/swagger/ui . 281 | 282 | ## Options 283 | 284 | ### Include Xml document file 285 | 286 | AzureFunctions.Extensions.Swashbuckle can include xml document file. 287 | 288 | 1. Change your functions project's GenerateDocumentationFile option to enable. 289 | 290 | builder.AddSwashBuckle(Assembly.GetExecutingAssembly(), opts => 291 | { 292 | opts.XmlPath = "TestFunction.xml"; 293 | }); 294 | 295 | 2. Add configration setting this extensions on your functions project's local.settings.json 296 | 297 | ```json 298 | "SwaggerDocOptions": { 299 | "XmlPath": "TestFunction.xml" 300 | } 301 | ``` 302 | 303 | Alternatively you can add this section to your host.json 304 | 305 | ```json 306 | "extensions": { 307 | "Swashbuckle": { 308 | "XmlPath": "TestFunction.xml" 309 | } 310 | } 311 | ``` 312 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/SwashbuckleConfig.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Compression; 2 | using System.Reflection; 3 | using System.Xml.XPath; 4 | using AzureFunctions.Extensions.Swashbuckle.Settings; 5 | using AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Extensions; 6 | using AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Filters; 7 | using AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Hosting; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Options; 12 | using Microsoft.OpenApi; 13 | using Microsoft.OpenApi.Extensions; 14 | using Microsoft.OpenApi.Models; 15 | using Swashbuckle.AspNetCore.Swagger; 16 | using Swashbuckle.AspNetCore.SwaggerGen; 17 | 18 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle 19 | { 20 | public sealed class SwashbuckleConfig 21 | { 22 | private const string ZippedResources = "EmbededResources.resources.zip"; 23 | private const string IndexHtmlName = "index.html"; 24 | private const string SwaggerUiName = "swagger-ui.css"; 25 | private const string SwaggerUiJsName = "swagger-ui-bundle.js"; 26 | private const string SwaggerUiJsPresetName = "swagger-ui-standalone-preset.js"; 27 | private const string SwaggerOAuth2RedirectName = "oauth2-redirect.html"; 28 | 29 | private static readonly Lazy IndexHtml = new Lazy(() => 30 | { 31 | var indexHtml = string.Empty; 32 | var assembly = GetAssembly(); 33 | 34 | using var stream = assembly.GetResourceByName(ZippedResources); 35 | if (stream != null) 36 | { 37 | using var archive = new ZipArchive(stream); 38 | 39 | indexHtml = LoadAndUpdateDocument(indexHtml, archive, IndexHtmlName); 40 | indexHtml = LoadAndUpdateDocument(indexHtml, archive, SwaggerUiName, "{style}"); 41 | indexHtml = LoadAndUpdateDocument(indexHtml, archive, SwaggerUiJsName, "{bundle.js}"); 42 | indexHtml = LoadAndUpdateDocument(indexHtml, archive, SwaggerUiJsPresetName, "{standalone-preset.js}"); 43 | } 44 | else 45 | { 46 | throw new ArgumentNullException(nameof(stream), "Embedded data stream is null"); 47 | } 48 | 49 | return indexHtml; 50 | }); 51 | 52 | private readonly IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider; 53 | 54 | private readonly Lazy indexHtmlLazy; 55 | private readonly Lazy oauth2RedirectLazy; 56 | private readonly SwaggerDocOptions swaggerOptions; 57 | private readonly string? xmlPath; 58 | private ServiceProvider? serviceProvider; 59 | 60 | public SwashbuckleConfig( 61 | IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider, 62 | IOptions swaggerDocOptions, 63 | SwashBuckleStartupConfig startupConfig) 64 | { 65 | this.apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider; 66 | this.swaggerOptions = swaggerDocOptions.Value; 67 | 68 | if (!string.IsNullOrWhiteSpace(this.swaggerOptions.XmlPath)) 69 | { 70 | var binPath = Path.GetDirectoryName(startupConfig.Assembly.Location); 71 | 72 | if (string.IsNullOrEmpty(binPath)) 73 | { 74 | throw new ArgumentNullException(nameof(binPath), "Not found path to an xml directory"); 75 | } 76 | 77 | var binDirectory = Directory.CreateDirectory(binPath); 78 | var xmlBasePath = binDirectory?.Parent?.FullName; 79 | 80 | if (xmlBasePath != null) 81 | { 82 | var path = Path.Combine(xmlBasePath, this.swaggerOptions.XmlPath); 83 | 84 | if (File.Exists(path)) 85 | { 86 | this.xmlPath = path; 87 | } 88 | } 89 | } 90 | 91 | this.indexHtmlLazy = new Lazy( 92 | () => IndexHtml.Value.Replace("{title}", this.swaggerOptions.Title)); 93 | 94 | this.oauth2RedirectLazy = new Lazy(() => 95 | { 96 | var assembly = GetAssembly(); 97 | using var stream = assembly.GetResourceByName(ZippedResources); 98 | using var archive = new ZipArchive(stream!); 99 | var entry = archive.GetEntry(SwaggerOAuth2RedirectName); 100 | using var entryStream = entry!.Open(); 101 | using var reader = new StreamReader(entryStream); 102 | return reader.ReadToEnd(); 103 | }); 104 | 105 | this.Initialize(); 106 | } 107 | 108 | public string RoutePrefix => this.swaggerOptions.RoutePrefix; 109 | 110 | public void Initialize() 111 | { 112 | var services = new ServiceCollection(); 113 | 114 | services.AddSingleton(this.apiDescriptionGroupCollectionProvider); 115 | 116 | services.AddSingleton(new FunctionHostingEnvironment()); 117 | 118 | if (this.swaggerOptions.AddNewtonsoftSupport) 119 | { 120 | services.AddSwaggerGenNewtonsoftSupport(); 121 | } 122 | 123 | services.AddSwaggerGen(options => 124 | { 125 | if (!this.swaggerOptions.Documents.Any()) 126 | { 127 | var defaultDocument = new SwaggerDocument(); 128 | AddSwaggerDocument(options, defaultDocument); 129 | } 130 | else 131 | { 132 | foreach (var optionDocument in this.swaggerOptions.Documents) 133 | { 134 | AddSwaggerDocument(options, optionDocument); 135 | } 136 | } 137 | 138 | if (!string.IsNullOrWhiteSpace(this.xmlPath)) 139 | { 140 | var xmlDoc = new XPathDocument(this.xmlPath); 141 | options.IncludeXmlComments(this.xmlPath); 142 | options.OperationFilter(xmlDoc); 143 | options.ParameterFilter(xmlDoc); 144 | options.SchemaFilter(xmlDoc); 145 | } 146 | 147 | options.OperationFilter(); 148 | options.OperationFilter(); 149 | options.OperationFilter(); 150 | options.OperationFilter(); 151 | 152 | this.swaggerOptions.ConfigureSwaggerGen?.Invoke(options); 153 | }); 154 | 155 | this.serviceProvider = services.BuildServiceProvider(true); 156 | } 157 | 158 | public string GetSwaggerOAuth2RedirectContent() 159 | { 160 | return this.oauth2RedirectLazy.Value; 161 | } 162 | 163 | public string GetSwaggerUIContent(string swaggerUrl) 164 | { 165 | if (this.swaggerOptions.OverridenPathToSwaggerJson != null) 166 | { 167 | swaggerUrl = this.swaggerOptions.OverridenPathToSwaggerJson.ToString(); 168 | } 169 | 170 | var html = this.indexHtmlLazy.Value; 171 | return html 172 | .Replace("{url}", swaggerUrl) 173 | .Replace("{oauth2RedirectUrl}", this.swaggerOptions.OAuth2RedirectPath) 174 | .Replace("{clientId}", this.swaggerOptions.ClientId) 175 | .Replace("{clientSecret}", this.swaggerOptions.ClientSecret) 176 | .Replace("{useBasicAuthenticationWithAccessCodeGrant}", this.swaggerOptions.UseBasicAuthenticationWithAccessCodeGrant ? "true" : "false") 177 | .Replace("{usePkceWithAuthorizationCodeGrant}", this.swaggerOptions.UsePkceWithAuthorizationCodeGrant ? "true" : "false"); 178 | } 179 | 180 | public Stream GetSwaggerJsonDocument(string host, string documentName = "v1") 181 | { 182 | var swaggerProvider = this.serviceProvider!.GetRequiredService(); 183 | var document = swaggerProvider.GetSwagger(documentName, host, string.Empty); 184 | return this.SerializeJsonDocument(document); 185 | } 186 | 187 | public Stream GetSwaggerYamlDocument(string host, string documentName = "v1") 188 | { 189 | var swaggerProvider = this.serviceProvider!.GetRequiredService(); 190 | var document = swaggerProvider.GetSwagger(documentName, host, string.Empty); 191 | return this.SerializeYamlDocument(document); 192 | } 193 | 194 | private static void AddSwaggerDocument(SwaggerGenOptions options, SwaggerDocument document) 195 | { 196 | options.SwaggerDoc(document.Name, new OpenApiInfo 197 | { 198 | Title = document.Title, 199 | Version = document.Version, 200 | Description = document.Description 201 | }); 202 | } 203 | private static Assembly GetAssembly() 204 | { 205 | var assembly = Assembly.GetAssembly(typeof(SwashBuckleClient)); 206 | 207 | if (assembly == null) 208 | { 209 | throw new ArgumentNullException(nameof(assembly)); 210 | } 211 | 212 | return assembly; 213 | } 214 | 215 | private static string LoadAndUpdateDocument( 216 | string documentHtml, 217 | ZipArchive archive, 218 | string entryName, 219 | string? replacement = null) 220 | { 221 | var entry = archive.GetEntry(entryName); 222 | using var stream = entry!.Open(); 223 | using var reader = new StreamReader(stream); 224 | var value = reader.ReadToEnd(); 225 | 226 | documentHtml = !string.IsNullOrEmpty(replacement) ? 227 | documentHtml.Replace(replacement, value) : 228 | value; 229 | 230 | return documentHtml; 231 | } 232 | 233 | private MemoryStream SerializeJsonDocument(OpenApiDocument document) 234 | { 235 | var memoryStream = new MemoryStream(); 236 | document.SerializeAsJson( 237 | memoryStream, 238 | this.swaggerOptions.SpecVersion == OpenApiSpecVersion.OpenApi2_0 ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0); 239 | 240 | memoryStream.Position = 0; 241 | return memoryStream; 242 | } 243 | 244 | private MemoryStream SerializeYamlDocument(OpenApiDocument document) 245 | { 246 | var memoryStream = new MemoryStream(); 247 | document.SerializeAsYaml( 248 | memoryStream, 249 | this.swaggerOptions.SpecVersion == OpenApiSpecVersion.OpenApi2_0 ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0); 250 | 251 | memoryStream.Position = 0; 252 | return memoryStream; 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/AzureFunctions.Extensions.Swashbuckle/SwashBuckle/Providers/FunctionApiDescriptionProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Reflection; 3 | using System.Text.RegularExpressions; 4 | using AzureFunctions.Extensions.Swashbuckle.Attribute; 5 | using AzureFunctions.Extensions.Swashbuckle.Settings; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.Abstractions; 9 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 10 | using Microsoft.AspNetCore.Mvc.Controllers; 11 | using Microsoft.AspNetCore.Mvc.Formatters; 12 | using Microsoft.AspNetCore.Mvc.ModelBinding; 13 | using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; 14 | using Microsoft.Azure.Functions.Worker; 15 | using Microsoft.Extensions.Logging; 16 | using Microsoft.Extensions.Options; 17 | 18 | namespace AzureFunctions.Extensions.Swashbuckle.SwashBuckle.Providers 19 | { 20 | internal class FunctionApiDescriptionProvider : IApiDescriptionGroupCollectionProvider 21 | { 22 | private readonly ICompositeMetadataDetailsProvider compositeMetadataDetailsProvider; 23 | private readonly IModelMetadataProvider modelMetadataProvider; 24 | private readonly SwaggerDocOptions swaggerDocOptions; 25 | private readonly IOutputFormatter outputFormatter; 26 | 27 | public FunctionApiDescriptionProvider( 28 | IOptions functionsOptions, 29 | SwashBuckleStartupConfig startupConfig, 30 | IModelMetadataProvider modelMetadataProvider, 31 | ICompositeMetadataDetailsProvider compositeMetadataDetailsProvider, 32 | IOutputFormatter outputFormatter) 33 | { 34 | this.swaggerDocOptions = functionsOptions.Value; 35 | this.modelMetadataProvider = modelMetadataProvider; 36 | this.compositeMetadataDetailsProvider = compositeMetadataDetailsProvider; 37 | this.outputFormatter = outputFormatter; 38 | 39 | var apiDescGroups = new Dictionary>(); 40 | var methods = startupConfig.Assembly.GetTypes() 41 | .SelectMany(t => t.GetMethods()) 42 | .Where(m => m.GetCustomAttributes(typeof(FunctionAttribute), false).Any()) 43 | .ToArray(); 44 | 45 | foreach (var methodInfo in methods) 46 | { 47 | if (!this.TryGetHttpTrigger(methodInfo, out var triggerAttribute)) 48 | { 49 | continue; 50 | } 51 | 52 | var functionAttr = (FunctionAttribute?)methodInfo.GetCustomAttribute(typeof(FunctionAttribute), false); 53 | 54 | if (functionAttr == null && triggerAttribute == null) 55 | { 56 | continue; 57 | } 58 | 59 | var apiExplorerSettingsAttribute = 60 | (ApiExplorerSettingsAttribute?)methodInfo.GetCustomAttribute(typeof(ApiExplorerSettingsAttribute), false) 61 | ?? (ApiExplorerSettingsAttribute?)methodInfo.DeclaringType!.GetCustomAttribute( 62 | typeof(ApiExplorerSettingsAttribute), 63 | false); 64 | 65 | var prefix = string.IsNullOrWhiteSpace(functionsOptions.Value.RoutePrefix) 66 | ? string.Empty 67 | : $"{functionsOptions.Value.RoutePrefix.TrimEnd('/')}/"; 68 | 69 | string? route = null; 70 | 71 | if (this.swaggerDocOptions.PrependOperationWithRoutePrefix) 72 | { 73 | var routePart = !string.IsNullOrWhiteSpace(triggerAttribute!.Route) 74 | ? triggerAttribute.Route 75 | : functionAttr!.Name; 76 | 77 | route = $"{prefix}{routePart}"; 78 | } 79 | else 80 | { 81 | route = !string.IsNullOrWhiteSpace(triggerAttribute!.Route) 82 | ? triggerAttribute.Route 83 | : functionAttr!.Name; 84 | } 85 | 86 | var routes = new List<(string Route, string RemoveParamName)>(); 87 | 88 | var regex = new Regex("/\\{(?\\w+)\\?\\}$"); 89 | var match = regex.Match(route!); 90 | 91 | var routeParamRemoveRegex = new Regex(":[a-zA-Z]+(\\(.*\\))?"); 92 | route = routeParamRemoveRegex.Replace(route!, string.Empty); 93 | 94 | if (match is { Success: true, Captures.Count: 1 }) 95 | { 96 | routes.Add( 97 | (route.Replace(match.Value, string.Empty).Replace("//", "/"), match.Groups["paramName"].ToString())); 98 | routes.Add((route.Replace(match.Value, match.Value.Replace("?", string.Empty)), string.Empty)); 99 | } 100 | else 101 | { 102 | routes.Add((route, string.Empty)); 103 | } 104 | 105 | var verbs = triggerAttribute.Methods ?? ["get", "post", "delete", "head", "patch", "put", "options"]; 106 | 107 | for (var index = 0; index < routes.Count; index++) 108 | { 109 | var (routeString, removeParamName) = routes[index]; 110 | var apiName = functionAttr!.Name + (index == 0 ? string.Empty : $"-{index}"); 111 | var items = verbs.Select(verb => this.CreateDescription( 112 | methodInfo, 113 | routeString, 114 | index, 115 | functionAttr, 116 | apiExplorerSettingsAttribute!, 117 | verb, 118 | triggerAttribute.AuthLevel, 119 | removeParamName, 120 | verbs.Length > 1)).ToArray(); 121 | 122 | var groupName = 123 | (items.FirstOrDefault()?.ActionDescriptor as ControllerActionDescriptor)?.ControllerName ?? 124 | apiName; 125 | if (!apiDescGroups.ContainsKey(groupName)) 126 | { 127 | apiDescGroups[groupName] = new List(); 128 | } 129 | 130 | apiDescGroups[groupName].AddRange(items); 131 | } 132 | } 133 | 134 | this.ApiDescriptionGroups = new ApiDescriptionGroupCollection( 135 | new ReadOnlyCollection( 136 | apiDescGroups.Select( 137 | kv => new ApiDescriptionGroup(kv.Key, kv.Value)).ToList()), 138 | 1); 139 | } 140 | 141 | public ApiDescriptionGroupCollection ApiDescriptionGroups { get; } 142 | 143 | private static void SetupDefaultJsonFormatterIfNone(IList supportedMediaTypes) 144 | { 145 | if (supportedMediaTypes.Count == 0) 146 | { 147 | supportedMediaTypes.Add(new ApiRequestFormat 148 | { 149 | MediaType = "application/json" 150 | }); 151 | } 152 | } 153 | 154 | private static Regex GetRoutePathParamRegex(string parameterName) 155 | { 156 | return new Regex("\\{[" + parameterName + "]+[\\?]{0,1}\\}"); 157 | } 158 | 159 | private bool TryGetHttpTrigger(MethodInfo methodInfo, out HttpTriggerAttribute? triggerAttribute) 160 | { 161 | triggerAttribute = null; 162 | var ignore = methodInfo.GetCustomAttributes().Any(x => x is SwaggerIgnoreAttribute); 163 | if (ignore) 164 | { 165 | return false; 166 | } 167 | 168 | triggerAttribute = this.FindHttpTriggerAttribute(methodInfo); 169 | 170 | if (triggerAttribute == null) 171 | { 172 | return false; 173 | } 174 | 175 | return true; 176 | } 177 | 178 | private ApiDescription CreateDescription( 179 | MethodInfo methodInfo, 180 | string route, 181 | int routeIndex, 182 | FunctionAttribute functionAttr, 183 | ApiExplorerSettingsAttribute apiExplorerSettingsAttr, 184 | string verb, 185 | AuthorizationLevel authorizationLevel, 186 | string? removeParamName = null, 187 | bool manyVerbs = false) 188 | { 189 | string controllerName; 190 | if (apiExplorerSettingsAttr?.GroupName != null) 191 | { 192 | controllerName = apiExplorerSettingsAttr.GroupName; 193 | } 194 | else 195 | { 196 | controllerName = methodInfo.DeclaringType!.Name.EndsWith("Controller") 197 | ? methodInfo.DeclaringType.Name.Remove( 198 | methodInfo.DeclaringType.Name.Length - "Controller".Length, "Controller".Length) 199 | : functionAttr.Name; 200 | } 201 | 202 | var actionName = functionAttr.Name; 203 | var actionNamePrefix = (routeIndex > 0 ? $"_{routeIndex}" : string.Empty) + (manyVerbs ? $"_{verb.ToLower()}" : string.Empty); 204 | 205 | var description = new ApiDescription 206 | { 207 | ActionDescriptor = new ControllerActionDescriptor 208 | { 209 | MethodInfo = methodInfo, 210 | ControllerName = controllerName, 211 | DisplayName = actionName, 212 | ControllerTypeInfo = methodInfo.DeclaringType!.GetTypeInfo(), 213 | Parameters = new List(), 214 | RouteValues = new Dictionary 215 | { 216 | { "controller", controllerName }, 217 | { "action", actionName } 218 | }, 219 | ActionName = !string.IsNullOrEmpty(actionNamePrefix) ? actionName + actionNamePrefix : string.Empty 220 | }, 221 | RelativePath = route, 222 | HttpMethod = verb.ToUpper() 223 | }; 224 | 225 | var supportedMediaTypes = methodInfo.GetCustomAttributes() 226 | .Select(x => new ApiRequestFormat { MediaType = x.MediaType }) 227 | .ToList(); 228 | 229 | SetupDefaultJsonFormatterIfNone(supportedMediaTypes); 230 | 231 | foreach (var supportedMediaType in supportedMediaTypes) 232 | { 233 | description.SupportedRequestFormats.Add(supportedMediaType); 234 | } 235 | 236 | var parameters = this.GetParametersDescription(methodInfo, route).ToList(); 237 | 238 | foreach (var parameter in parameters.Where(parameter => parameter.Name != removeParamName)) 239 | { 240 | description.ActionDescriptor.Parameters.Add(new ParameterDescriptor 241 | { 242 | Name = parameter.Name, 243 | ParameterType = parameter.Type 244 | }); 245 | description.ParameterDescriptions.Add(parameter); 246 | } 247 | 248 | foreach (var apiResponseType in this.GetResponseTypes(methodInfo)) 249 | { 250 | description.SupportedResponseTypes.Add(apiResponseType); 251 | } 252 | 253 | if (this.swaggerDocOptions.AddCodeParameter && authorizationLevel != AuthorizationLevel.Anonymous) 254 | { 255 | description.ParameterDescriptions.Add(new ApiParameterDescription 256 | { 257 | Name = "code", 258 | Type = typeof(string), 259 | Source = BindingSource.Query, 260 | RouteInfo = new ApiParameterRouteInfo 261 | { 262 | IsOptional = true 263 | } 264 | }); 265 | } 266 | 267 | return description; 268 | } 269 | 270 | private IEnumerable GetResponseTypes(MethodInfo methodInfo) 271 | { 272 | return methodInfo.GetCustomAttributes(typeof(ProducesResponseTypeAttribute)) 273 | .Select(customAttribute => customAttribute as ProducesResponseTypeAttribute) 274 | .Select(responseType => 275 | { 276 | var isVoidResponseType = responseType!.Type == typeof(void); 277 | 278 | return new ApiResponseType 279 | { 280 | ApiResponseFormats = new[] 281 | { 282 | new ApiResponseFormat 283 | { 284 | Formatter = this.outputFormatter, 285 | MediaType = "application/json" 286 | } 287 | }, 288 | ModelMetadata = isVoidResponseType ? null : this.modelMetadataProvider.GetMetadataForType(responseType.Type), 289 | Type = isVoidResponseType ? null : responseType.Type, 290 | StatusCode = responseType.StatusCode 291 | }; 292 | }); 293 | } 294 | 295 | private HttpTriggerAttribute? FindHttpTriggerAttribute(MethodInfo methodInfo) 296 | { 297 | HttpTriggerAttribute? triggerAttribute = null; 298 | 299 | foreach (var parameter in methodInfo.GetParameters()) 300 | { 301 | triggerAttribute = parameter.GetCustomAttributes(typeof(HttpTriggerAttribute), false) 302 | .FirstOrDefault() as HttpTriggerAttribute; 303 | if (triggerAttribute != null) 304 | { 305 | break; 306 | } 307 | } 308 | 309 | return triggerAttribute; 310 | } 311 | 312 | private IEnumerable GetParametersDescription(MethodInfo methodInfo, string route) 313 | { 314 | foreach (var parameter in methodInfo.GetParameters()) 315 | { 316 | var requestBodyTypeAttribute = 317 | parameter.GetCustomAttribute(typeof(RequestBodyTypeAttribute)) as RequestBodyTypeAttribute; 318 | 319 | if ((parameter.ParameterType == typeof(HttpRequestMessage) || 320 | parameter.ParameterType == typeof(HttpRequest)) 321 | && requestBodyTypeAttribute == null) 322 | { 323 | continue; 324 | } 325 | 326 | if (this.IgnoreParameter(parameter)) 327 | { 328 | continue; 329 | } 330 | 331 | var hasHttpTriggerAttribute = parameter.GetCustomAttributes() 332 | .Any(attr => attr is HttpTriggerAttribute); 333 | 334 | var hasFromUriAttribute = false; 335 | 336 | var type = hasHttpTriggerAttribute && requestBodyTypeAttribute != null 337 | ? requestBodyTypeAttribute.Type 338 | : parameter.ParameterType; 339 | 340 | var regex = GetRoutePathParamRegex(parameter.Name!); 341 | var match = regex.Match(route); 342 | var bindingSource = match.Success ? BindingSource.Path 343 | : hasFromUriAttribute ? BindingSource.Query 344 | : BindingSource.Body; 345 | 346 | var optional = bindingSource == BindingSource.Query || match.Value.Contains("?"); 347 | 348 | yield return new ApiParameterDescription 349 | { 350 | Name = parameter.Name!, 351 | Type = type, 352 | Source = bindingSource, 353 | RouteInfo = new ApiParameterRouteInfo 354 | { 355 | IsOptional = optional 356 | }, 357 | ModelMetadata = new DefaultModelMetadata( 358 | this.modelMetadataProvider, 359 | this.compositeMetadataDetailsProvider, 360 | new DefaultMetadataDetails( 361 | ModelMetadataIdentity.ForType(type), 362 | ModelAttributes.GetAttributesForType(type))) 363 | }; 364 | } 365 | } 366 | 367 | private bool IgnoreParameter(ParameterInfo parameter) 368 | { 369 | var ignoreParameterAttribute = parameter.GetCustomAttribute(typeof(SwaggerIgnoreAttribute)); 370 | if (ignoreParameterAttribute != null) 371 | { 372 | return true; 373 | } 374 | 375 | if (parameter.ParameterType.Name == "TraceWriter") 376 | { 377 | return true; 378 | } 379 | 380 | if (parameter.ParameterType == typeof(ExecutionContext)) 381 | { 382 | return true; 383 | } 384 | 385 | if (parameter.ParameterType == typeof(ILogger)) 386 | { 387 | return true; 388 | } 389 | 390 | if (parameter.ParameterType.IsAssignableFrom(typeof(ILogger))) 391 | { 392 | return true; 393 | } 394 | 395 | if (parameter.ParameterType.IsAssignableFrom(typeof(ISwashBuckleClient))) 396 | { 397 | return true; 398 | } 399 | 400 | if (parameter.GetCustomAttributes().Any(attr => attr is HttpTriggerAttribute) 401 | && parameter.GetCustomAttributes().All(attr => !(attr is RequestBodyTypeAttribute))) 402 | { 403 | return true; 404 | } 405 | 406 | return false; 407 | } 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/AzureFunctions.Extensions.Swashbuckle/.editorconfig: -------------------------------------------------------------------------------- 1 | # Version: 1.3.1 (Using https://semver.org/) 2 | # Updated: 2019-08-04 3 | # See https://github.com/RehanSaeed/EditorConfig/releases for release notes. 4 | # See https://github.com/RehanSaeed/EditorConfig for updates to this file. 5 | # See http://EditorConfig.org for more information about .editorconfig files. 6 | 7 | ########################################## 8 | # Common Settings 9 | ########################################## 10 | 11 | # This file is the top-most EditorConfig file 12 | root = true 13 | 14 | # All Files 15 | [*] 16 | charset = utf-8 17 | indent_style = space 18 | indent_size = 4 19 | insert_final_newline = true 20 | trim_trailing_whitespace = true 21 | 22 | ########################################## 23 | # File Extension Settings 24 | ########################################## 25 | 26 | # Visual Studio Solution Files 27 | [*.sln] 28 | indent_style = tab 29 | 30 | # Visual Studio XML Project Files 31 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 32 | indent_size = 2 33 | 34 | # Various XML Configuration Files 35 | [*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}] 36 | indent_size = 2 37 | 38 | # JSON Files 39 | [*.{json,json5}] 40 | indent_size = 2 41 | 42 | # YAML Files 43 | [*.{yml,yaml}] 44 | indent_size = 2 45 | 46 | # Markdown Files 47 | [*.md] 48 | trim_trailing_whitespace = false 49 | 50 | # Web Files 51 | [*.{htm,html,js,ts,tsx,css,sass,scss,less,svg,vue}] 52 | indent_size = 2 53 | 54 | # Batch Files 55 | [*.{cmd,bat}] 56 | 57 | # Bash Files 58 | [*.sh] 59 | end_of_line = lf 60 | 61 | ########################################## 62 | # .NET Language Conventions 63 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions 64 | ########################################## 65 | 66 | # .NET Code Style Settings 67 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings 68 | [*.{cs,csx,cake,vb}] 69 | # "this." and "Me." qualifiers 70 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me 71 | dotnet_style_qualification_for_field = true:warning 72 | dotnet_style_qualification_for_property = true:warning 73 | dotnet_style_qualification_for_method = true:warning 74 | dotnet_style_qualification_for_event = true:warning 75 | # Language keywords instead of framework type names for type references 76 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords 77 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 78 | dotnet_style_predefined_type_for_member_access = true:warning 79 | # Modifier preferences 80 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers 81 | dotnet_style_require_accessibility_modifiers = always:warning 82 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 83 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async 84 | dotnet_style_readonly_field = true:warning 85 | # Parentheses preferences 86 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences 87 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning 88 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning 89 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning 90 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion 91 | # Expression-level preferences 92 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences 93 | dotnet_style_object_initializer = true:warning 94 | dotnet_style_collection_initializer = true:warning 95 | dotnet_style_explicit_tuple_names = true:warning 96 | dotnet_style_prefer_inferred_tuple_names = true:warning 97 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning 98 | dotnet_style_prefer_auto_properties = true:warning 99 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning 100 | dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion 101 | dotnet_style_prefer_conditional_expression_over_return = false:suggestion 102 | dotnet_style_prefer_compound_assignment = true:warning 103 | # Null-checking preferences 104 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences 105 | dotnet_style_coalesce_expression = true:warning 106 | dotnet_style_null_propagation = true:warning 107 | # Parameter preferences 108 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences 109 | dotnet_code_quality_unused_parameters = all:warning 110 | # More style options (Undocumented) 111 | # https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641 112 | dotnet_style_operator_placement_when_wrapping = end_of_line 113 | 114 | # C# Code Style Settings 115 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings 116 | [*.{cs,csx,cake}] 117 | # Implicit and explicit types 118 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types 119 | csharp_style_var_for_built_in_types = true:warning 120 | csharp_style_var_when_type_is_apparent = true:warning 121 | csharp_style_var_elsewhere = true:warning 122 | # Expression-bodied members 123 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members 124 | # IDE0022 125 | csharp_style_expression_bodied_methods = false:silent 126 | # IDE0021 127 | csharp_style_expression_bodied_constructors = false:silent 128 | # IDE0023, IDE0024 129 | csharp_style_expression_bodied_operators = false:silent 130 | # IDE0025 131 | csharp_style_expression_bodied_properties = true:silent 132 | # IDE0026 133 | csharp_style_expression_bodied_indexers = true:silent 134 | # IDE0027 135 | csharp_style_expression_bodied_accessors = true:silent 136 | # IDE0053 137 | csharp_style_expression_bodied_lambdas = true:silent 138 | # IDE0061 139 | csharp_style_expression_bodied_local_functions = false:silent 140 | # Pattern matching 141 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching 142 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 143 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 144 | # Inlined variable declarations 145 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations 146 | csharp_style_inlined_variable_declaration = true:warning 147 | # Expression-level preferences 148 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences 149 | csharp_prefer_simple_default_expression = true:warning 150 | # "Null" checking preferences 151 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences 152 | csharp_style_throw_expression = true:warning 153 | csharp_style_conditional_delegate_call = true:warning 154 | # Code block preferences 155 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences 156 | csharp_prefer_braces = true:warning 157 | # Unused value preferences 158 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences 159 | csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion 160 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 161 | # Index and range preferences 162 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences 163 | csharp_style_prefer_index_operator = true:warning 164 | csharp_style_prefer_range_operator = true:warning 165 | # Miscellaneous preferences 166 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences 167 | csharp_style_deconstructed_variable_declaration = true:warning 168 | csharp_style_pattern_local_over_anonymous_function = true:warning 169 | csharp_using_directive_placement = outside_namespace:warning 170 | csharp_prefer_static_local_function = true:warning 171 | csharp_prefer_simple_using_statement = false:warning 172 | 173 | ########################################## 174 | # .NET Formatting Conventions 175 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions 176 | ########################################## 177 | 178 | # Organize usings 179 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives 180 | dotnet_sort_system_directives_first = always:error 181 | dotnet_separate_import_directive_groups = always:error 182 | # Newline options 183 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options 184 | csharp_new_line_before_open_brace = all 185 | csharp_new_line_before_else = true 186 | csharp_new_line_before_catch = true 187 | csharp_new_line_before_finally = true 188 | csharp_new_line_before_members_in_object_initializers = true 189 | csharp_new_line_before_members_in_anonymous_types = true 190 | csharp_new_line_between_query_expression_clauses = true 191 | # Indentation options 192 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options 193 | csharp_indent_case_contents = true 194 | csharp_indent_switch_labels = true 195 | csharp_indent_labels = no_change 196 | csharp_indent_block_contents = true 197 | csharp_indent_braces = false 198 | csharp_indent_case_contents_when_block = false 199 | # Spacing options 200 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options 201 | csharp_space_after_cast = false 202 | csharp_space_after_keywords_in_control_flow_statements = true 203 | csharp_space_between_parentheses = false 204 | csharp_space_before_colon_in_inheritance_clause = true 205 | csharp_space_after_colon_in_inheritance_clause = true 206 | csharp_space_around_binary_operators = before_and_after 207 | csharp_space_between_method_declaration_parameter_list_parentheses = false 208 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 209 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 210 | csharp_space_between_method_call_parameter_list_parentheses = false 211 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 212 | csharp_space_between_method_call_name_and_opening_parenthesis = false 213 | csharp_space_after_comma = true 214 | csharp_space_before_comma = false 215 | csharp_space_after_dot = false 216 | csharp_space_before_dot = false 217 | csharp_space_after_semicolon_in_for_statement = true 218 | csharp_space_before_semicolon_in_for_statement = false 219 | csharp_space_around_declaration_statements = false 220 | csharp_space_before_open_square_brackets = false 221 | csharp_space_between_empty_square_brackets = false 222 | csharp_space_between_square_brackets = false 223 | # Wrapping options 224 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options 225 | csharp_preserve_single_line_statements = false 226 | csharp_preserve_single_line_blocks = true 227 | 228 | ########################################## 229 | # .NET Naming Conventions 230 | # https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions 231 | ########################################## 232 | 233 | [*.{cs,csx,cake,vb}] 234 | 235 | ########################################## 236 | # Styles 237 | ########################################## 238 | 239 | # camel_case_style - Define the camelCase style 240 | dotnet_naming_style.camel_case_style.capitalization = camel_case 241 | # pascal_case_style - Define the PascalCase style 242 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 243 | # first_upper_style - The first character must start with an upper-case character 244 | dotnet_naming_style.first_upper_style.capitalization = first_word_upper 245 | # prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I' 246 | dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case 247 | dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I 248 | # prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T' 249 | dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case 250 | dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T 251 | # disallowed_style - Anything that has this style applied is marked as disallowed 252 | dotnet_naming_style.disallowed_style.capitalization = pascal_case 253 | dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____ 254 | dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____ 255 | # internal_error_style - This style should never occur... if it does, it's indicates a bug in file or in the parser using the file 256 | dotnet_naming_style.internal_error_style.capitalization = pascal_case 257 | dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____ 258 | dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____ 259 | 260 | ########################################## 261 | # .NET Design Guideline Field Naming Rules 262 | # Naming rules for fields follow the .NET Framework design guidelines 263 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/index 264 | ########################################## 265 | 266 | # All public/protected/protected_internal constant fields must be PascalCase 267 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 268 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal 269 | dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const 270 | dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field 271 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group 272 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style 273 | dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning 274 | 275 | # All public/protected/protected_internal static readonly fields must be PascalCase 276 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 277 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal 278 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly 279 | dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field 280 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group 281 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style 282 | dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning 283 | 284 | # No other public/protected/protected_internal fields are allowed 285 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/field 286 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal 287 | dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field 288 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group 289 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style 290 | dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error 291 | 292 | ########################################## 293 | # StyleCop Field Naming Rules 294 | # Naming rules for fields follow the StyleCop analyzers 295 | # This does not override any rules using disallowed_style above 296 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers 297 | ########################################## 298 | 299 | # All constant fields must be PascalCase 300 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md 301 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 302 | dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const 303 | dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field 304 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group 305 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style 306 | dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning 307 | 308 | # All static readonly fields must be PascalCase 309 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md 310 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private 311 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly 312 | dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field 313 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group 314 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style 315 | dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning 316 | 317 | # No non-private instance fields are allowed 318 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md 319 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected 320 | dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field 321 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group 322 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style 323 | dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error 324 | 325 | # Private fields must be camelCase 326 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md 327 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private 328 | dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field 329 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group 330 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style 331 | dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning 332 | 333 | # Local variables must be camelCase 334 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md 335 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local 336 | dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local 337 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group 338 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style 339 | dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent 340 | 341 | # This rule should never fire. However, it's included for at least two purposes: 342 | # First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. 343 | # Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). 344 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * 345 | dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field 346 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group 347 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style 348 | dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error 349 | 350 | 351 | ########################################## 352 | # Other Naming Rules 353 | ########################################## 354 | 355 | # All of the following must be PascalCase: 356 | # - Namespaces 357 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces 358 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md 359 | # - Classes and Enumerations 360 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 361 | # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md 362 | # - Delegates 363 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types 364 | # - Constructors, Properties, Events, Methods 365 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members 366 | dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property 367 | dotnet_naming_rule.element_rule.symbols = element_group 368 | dotnet_naming_rule.element_rule.style = pascal_case_style 369 | dotnet_naming_rule.element_rule.severity = warning 370 | 371 | # Interfaces use PascalCase and are prefixed with uppercase 'I' 372 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 373 | dotnet_naming_symbols.interface_group.applicable_kinds = interface 374 | dotnet_naming_rule.interface_rule.symbols = interface_group 375 | dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style 376 | dotnet_naming_rule.interface_rule.severity = warning 377 | 378 | # Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' 379 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces 380 | dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter 381 | dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group 382 | dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style 383 | dotnet_naming_rule.type_parameter_rule.severity = warning 384 | 385 | # Function parameters use camelCase 386 | # https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters 387 | dotnet_naming_symbols.parameters_group.applicable_kinds = parameter 388 | dotnet_naming_rule.parameters_rule.symbols = parameters_group 389 | dotnet_naming_rule.parameters_rule.style = camel_case_style 390 | dotnet_naming_rule.parameters_rule.severity = warning 391 | 392 | ########################################## 393 | # License 394 | ########################################## 395 | # The following applies as to the .editorconfig file ONLY, and is 396 | # included below for reference, per the requirements of the license 397 | # corresponding to this .editorconfig file. 398 | # See: https://github.com/RehanSaeed/EditorConfig 399 | # 400 | # MIT License 401 | # 402 | # Copyright (c) 2017-2019 Muhammad Rehan Saeed 403 | # Copyright (c) 2019 Henry Gabryjelski 404 | # 405 | # Permission is hereby granted, free of charge, to any 406 | # person obtaining a copy of this software and associated 407 | # documentation files (the "Software"), to deal in the 408 | # Software without restriction, including without limitation 409 | # the rights to use, copy, modify, merge, publish, distribute, 410 | # sublicense, and/or sell copies of the Software, and to permit 411 | # persons to whom the Software is furnished to do so, subject 412 | # to the following conditions: 413 | # 414 | # The above copyright notice and this permission notice shall be 415 | # included in all copies or substantial portions of the Software. 416 | # 417 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 418 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 419 | # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 420 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 421 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 422 | # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 423 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 424 | # OTHER DEALINGS IN THE SOFTWARE. 425 | ########################################## 426 | --------------------------------------------------------------------------------