├── .gitignore ├── LICENSE.md ├── Logo.png ├── README.md └── SignalRSwaggerGen ├── .editorconfig ├── SignalRSwaggerGen.sln ├── SignalRSwaggerGen ├── Attributes │ ├── SignalRHiddenAttribute.cs │ ├── SignalRHubAttribute.cs │ ├── SignalRMethodAttribute.cs │ ├── SignalRParamAttribute.cs │ ├── SignalRRequestBodyAttribute.cs │ └── SignalRReturnAttribute.cs ├── Constants.cs ├── Enums │ ├── AutoDiscover.cs │ └── Operation.cs ├── Naming │ ├── NameTransformer.cs │ ├── ToCamelTransformer.cs │ ├── ToLowerTransformer.cs │ └── ToUpperTransformer.cs ├── SignalRSwaggerGen.cs ├── SignalRSwaggerGen.csproj ├── SignalRSwaggerGen.xml ├── SignalRSwaggerGenOptions.cs ├── SwaggerGenOptionsExtensions.cs └── Utils │ ├── Comparison │ └── SignalRReturnAttributeComparer.cs │ ├── EnumerableUtils.cs │ ├── ReflectionUtils.cs │ ├── StringUtils.cs │ └── XmlComments │ ├── XmlComments.cs │ └── XmlCommentsUtils.cs └── TestWebApi ├── Controllers └── TestController.cs ├── Hubs ├── StronglyTypedTestHub.cs ├── TestHub.cs └── XmlCommentsHub.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Startup.cs ├── TestWebApi.csproj ├── TestWebApi.xml ├── WeatherForecast.cs ├── appsettings.Development.json └── appsettings.json /.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dorin Mocan 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 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/essencebit/SignalRSwaggerGen/ff9c7fe94d8027dd5c1607d26c2f05611a967209/Logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Go to wiki: https://github.com/essencebit/SignalRSwaggerGen/wiki 2 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CS1591: Missing XML comment for publicly visible type or member 4 | dotnet_diagnostic.CS1591.severity = silent 5 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31306.274 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SignalRSwaggerGen", "SignalRSwaggerGen\SignalRSwaggerGen.csproj", "{7CB27570-0187-4ED6-AC83-81D79C6E89F7}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F50A6213-4D74-41A0-B4F2-6913378CE896}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebApi", "TestWebApi\TestWebApi.csproj", "{F75822CA-1FEF-45B9-8D63-B3F8789448C3}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {7CB27570-0187-4ED6-AC83-81D79C6E89F7}.Debug|Any CPU.ActiveCfg = Release|Any CPU 22 | {7CB27570-0187-4ED6-AC83-81D79C6E89F7}.Debug|Any CPU.Build.0 = Release|Any CPU 23 | {7CB27570-0187-4ED6-AC83-81D79C6E89F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {7CB27570-0187-4ED6-AC83-81D79C6E89F7}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {F75822CA-1FEF-45B9-8D63-B3F8789448C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {F75822CA-1FEF-45B9-8D63-B3F8789448C3}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {F75822CA-1FEF-45B9-8D63-B3F8789448C3}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {F75822CA-1FEF-45B9-8D63-B3F8789448C3}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {A2754AA3-AF10-4DF9-88EB-49F150583FD3} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Attributes/SignalRHiddenAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SignalRSwaggerGen.Attributes 4 | { 5 | /// 6 | /// Use this attribute to disable Swagger documentation for specific components 7 | /// 8 | [AttributeUsage( 9 | AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct 10 | | AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.ReturnValue, 11 | AllowMultiple = false, Inherited = false)] 12 | public sealed class SignalRHiddenAttribute : Attribute 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Attributes/SignalRHubAttribute.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Enums; 2 | using SignalRSwaggerGen.Naming; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace SignalRSwaggerGen.Attributes 8 | { 9 | /// 10 | /// Use this attribute to enable Swagger documentation for hubs 11 | /// 12 | [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] 13 | public sealed class SignalRHubAttribute : Attribute 14 | { 15 | public string Path { get; } 16 | public AutoDiscover AutoDiscover { get; } 17 | public IEnumerable DocumentNames { get; } 18 | public NameTransformer NameTransformer { get; } 19 | public string Tag { get; } 20 | public bool XmlCommentsDisabled { get; } 21 | 22 | /// Path of the hub. If path contains "[Hub]", this part will be replaced with the name of the type holding this attribute(hub name). 23 | /// If not specified, the func from 'SignalRSwaggerGenOptions' will be used to get the path. 24 | /// A flag indicating what components will have Swagger documentation enabled automatically. 25 | /// If 'AutoDiscover.Inherit' specified, the value from 'SignalRSwaggerGenOptions' will be used. 26 | /// An array of names of the Swagger documents the hub will be displayed in. 27 | /// If null specified, then the value from 'SignalRSwaggerGenOptions' will be used. If empty array specified, then the hub will be displayed in all documents. 28 | /// The type of the name transformer. The type must inherit from 'SignalRSwaggerGen.Naming.NameTransformer' class, be non-abstract and have public parameterless constructor. 29 | /// The name transformer will be used to transform the name of the hub and its methods. If null specified, the transformer from 'SignalRSwaggerGenOptions' will be used. 30 | /// The namespace 'SignalRSwaggerGen.Naming' already contains some predefined name transformers, so check 'em out. 31 | /// The tag under which the hub will be placed in Swagger doc. If null specified, the summary section of the XML comments of the hub will be used. 32 | /// If XML comments missing or not enabled, the name of the type holding this attribute will be used. 33 | /// A flag indicating if XML comments are disabled for the hub 34 | /// Thrown if 35 | /// - value not allowed for this attribute 36 | /// - is abstract or does not inherit from 'SignalRSwaggerGen.Naming.NameTransformer' class or has no public parameterless constructor 37 | public SignalRHubAttribute( 38 | string path = null, 39 | AutoDiscover autoDiscover = AutoDiscover.Inherit, 40 | string[] documentNames = null, 41 | Type nameTransformerType = null, 42 | string tag = null, 43 | bool xmlCommentsDisabled = false) 44 | { 45 | if (!_validAutoDiscoverValues.Contains(autoDiscover)) throw new ArgumentException($"Value {autoDiscover} not allowed for this attribute", nameof(autoDiscover)); 46 | ValidateNameTransformerType(nameTransformerType); 47 | Path = path; 48 | AutoDiscover = autoDiscover; 49 | DocumentNames = documentNames; 50 | NameTransformer = nameTransformerType == null ? null : (NameTransformer)Activator.CreateInstance(nameTransformerType); 51 | Tag = tag; 52 | XmlCommentsDisabled = xmlCommentsDisabled; 53 | } 54 | 55 | private static void ValidateNameTransformerType(Type nameTransformerType) 56 | { 57 | if (nameTransformerType == null) return; 58 | if (nameTransformerType.IsAbstract) throw new ArgumentException($"Type {nameTransformerType.Name} is abstract", nameof(nameTransformerType)); 59 | if (!nameTransformerType.IsSubclassOf(typeof(NameTransformer))) throw new ArgumentException($"Type {nameTransformerType.Name} is not a subtype of {typeof(NameTransformer).Name}", nameof(nameTransformerType)); 60 | if (nameTransformerType.GetConstructor(Type.EmptyTypes) == null) throw new ArgumentException($"Type {nameTransformerType.Name} has no public parameterless constructor", nameof(nameTransformerType)); 61 | } 62 | 63 | private static readonly IEnumerable _validAutoDiscoverValues = new List 64 | { 65 | AutoDiscover.Inherit, 66 | AutoDiscover.None, 67 | AutoDiscover.Methods, 68 | AutoDiscover.Params, 69 | AutoDiscover.MethodsAndParams, 70 | }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Attributes/SignalRMethodAttribute.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Enums; 2 | using SignalRSwaggerGen.Utils; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace SignalRSwaggerGen.Attributes 8 | { 9 | /// 10 | /// Use this attribute to enable Swagger documentation for hub methods 11 | /// 12 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 13 | public sealed class SignalRMethodAttribute : Attribute 14 | { 15 | public string Name { get; } 16 | public Operation Operation { get; } 17 | public AutoDiscover AutoDiscover { get; } 18 | public string Summary { get; } 19 | public string Description { get; } 20 | 21 | /// Name of the method which will be invoked on the client side. 22 | /// If name contains "[Method]", this part will be replaced with the name of the method holding this attribute. 23 | /// Same as HTTP verb. If 'Operation.Inherit' specified, then the value from 'SignalRSwaggerGenOptions' will be used. 24 | /// A flag indicating what components will have Swagger documentation enabled automatically. 25 | /// If 'AutoDiscover.Inherit' specified, the value from the hub will be used. If the hub also has this value specified, then the value from 'SignalRSwaggerGenOptions' will be used. 26 | /// The text that will appear in summary section of the decorated method in Swagger document. 27 | /// If null specified and XML comments not disabled, the summary section of the XML comments of the method will be used. 28 | /// The text that will appear in description section of decorated method in Swagger document 29 | /// Thrown if 30 | /// - is null or empty 31 | /// - value not allowed for this attribute 32 | public SignalRMethodAttribute( 33 | string name = Constants.MethodNamePlaceholder, 34 | Operation operation = Operation.Inherit, 35 | AutoDiscover autoDiscover = AutoDiscover.Inherit, 36 | string summary = null, 37 | string description = null) 38 | { 39 | if (name.IsNullOrEmpty()) throw new ArgumentException("Name is null or empty", nameof(name)); 40 | if (!_validAutoDiscoverValues.Contains(autoDiscover)) throw new ArgumentException($"Value {autoDiscover} not allowed for this attribute", nameof(autoDiscover)); 41 | Name = name; 42 | Operation = operation; 43 | AutoDiscover = autoDiscover; 44 | Summary = summary; 45 | Description = description; 46 | } 47 | 48 | private static readonly IEnumerable _validAutoDiscoverValues = new List 49 | { 50 | AutoDiscover.Inherit, 51 | AutoDiscover.None, 52 | AutoDiscover.Params, 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Attributes/SignalRParamAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SignalRSwaggerGen.Attributes 4 | { 5 | /// 6 | /// Use this attribute to enable Swagger documentation for method params 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] 9 | public sealed class SignalRParamAttribute : Attribute 10 | { 11 | public string Description { get; } 12 | public Type ParamType { get; } 13 | 14 | /// The text that will appear in description section of decorated parameter in Swagger document 15 | /// Parameter type. If null specified, the type of the parameter holding this attribute will be picked up. 16 | public SignalRParamAttribute(string description = null, Type paramType = null) 17 | { 18 | Description = description; 19 | ParamType = paramType; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Attributes/SignalRRequestBodyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SignalRSwaggerGen.Attributes 4 | { 5 | /// 6 | /// Use this attribute to enable Swagger documentation for request body 7 | /// 8 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 9 | public class SignalRRequestBodyAttribute : Attribute 10 | { 11 | public Type BodyType { get; } 12 | public bool IsRequired { get; } 13 | public string Description { get; } 14 | 15 | /// The type of the request body 16 | /// The value that indicates if the request body is required or not 17 | /// The text that will appear in description section of the corresponding request body in Swagger document 18 | /// Thrown if is null 19 | public SignalRRequestBodyAttribute(Type bodyType, bool isRequired = false, string description = null) 20 | { 21 | BodyType = bodyType ?? throw new ArgumentNullException(nameof(bodyType)); 22 | IsRequired = isRequired; 23 | Description = description; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Attributes/SignalRReturnAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SignalRSwaggerGen.Attributes 4 | { 5 | /// 6 | /// Use this attribute to enable Swagger documentation for method return type 7 | /// 8 | [AttributeUsage(AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] 9 | public sealed class SignalRReturnAttribute : Attribute 10 | { 11 | public Type ReturnType { get; } 12 | public int StatusCode { get; } 13 | public string Description { get; } 14 | 15 | /// Return type. If null specified, the return type of the method will be picked up. 16 | /// The text that will appear in status code section of the corresponding response in Swagger document 17 | /// The text that will appear in description section of the corresponding response in Swagger document 18 | public SignalRReturnAttribute(Type returnType = null, int statusCode = 200, string description = "Success") 19 | { 20 | ReturnType = returnType; 21 | StatusCode = statusCode; 22 | Description = description; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Constants.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Enums; 2 | 3 | namespace SignalRSwaggerGen 4 | { 5 | public class Constants 6 | { 7 | public const string HubNamePlaceholder = "[Hub]"; 8 | public const string MethodNamePlaceholder = "[Method]"; 9 | public const string DefaultHubPathTemplate = "/hubs/" + HubNamePlaceholder; 10 | public const AutoDiscover DefaultAutoDiscover = AutoDiscover.MethodsAndParams; 11 | public const Operation DefaultOperation = Operation.Post; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Enums/AutoDiscover.cs: -------------------------------------------------------------------------------- 1 | namespace SignalRSwaggerGen.Enums 2 | { 3 | /// 4 | /// A flag indicating what components will have Swagger documentation enabled automatically 5 | /// 6 | public enum AutoDiscover 7 | { 8 | /// 9 | /// Inherit value from higher level configurations 10 | /// 11 | Inherit = -1, 12 | 13 | /// 14 | /// None 15 | /// 16 | None = 0, 17 | 18 | /// 19 | /// Public non-static methods 20 | /// 21 | Methods = 1, 22 | 23 | /// 24 | /// Method params 25 | /// 26 | Params = 2, 27 | 28 | /// 29 | /// Public non-static methods and their params 30 | /// 31 | MethodsAndParams = 3, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Enums/Operation.cs: -------------------------------------------------------------------------------- 1 | namespace SignalRSwaggerGen.Enums 2 | { 3 | /// 4 | /// Same as HTTP verb 5 | /// 6 | public enum Operation 7 | { 8 | /// 9 | /// Inherit value from higher level configurations 10 | /// 11 | Inherit = -1, 12 | Get = 0, 13 | Put = 1, 14 | Post = 2, 15 | Delete = 3, 16 | Options = 4, 17 | Head = 5, 18 | Patch = 6, 19 | Trace = 7, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Naming/NameTransformer.cs: -------------------------------------------------------------------------------- 1 | namespace SignalRSwaggerGen.Naming 2 | { 3 | /// 4 | /// Inherit from this class in order to create a name transformer 5 | /// 6 | public abstract class NameTransformer 7 | { 8 | /// 9 | /// Name transformation function 10 | /// 11 | /// The name to transform 12 | /// Transformed name 13 | public abstract string Transform(string name); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Naming/ToCamelTransformer.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Utils; 2 | 3 | namespace SignalRSwaggerGen.Naming 4 | { 5 | public class ToCamelTransformer : NameTransformer 6 | { 7 | /// 8 | /// Transforms the name to camel case 9 | /// 10 | /// The name to transform 11 | /// Camel case name 12 | public override string Transform(string name) 13 | { 14 | if (name.IsNullOrEmpty()) return name; 15 | return char.ToLowerInvariant(name[0]) + name.Substring(1); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Naming/ToLowerTransformer.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Utils; 2 | 3 | namespace SignalRSwaggerGen.Naming 4 | { 5 | public class ToLowerTransformer : NameTransformer 6 | { 7 | /// 8 | /// Transforms the name to lower case 9 | /// 10 | /// The name to transform 11 | /// Lower case name 12 | public override string Transform(string name) 13 | { 14 | if (name.IsNullOrEmpty()) return name; 15 | return name.ToLowerInvariant(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Naming/ToUpperTransformer.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Utils; 2 | 3 | namespace SignalRSwaggerGen.Naming 4 | { 5 | public class ToUpperTransformer : NameTransformer 6 | { 7 | /// 8 | /// Transforms the name to upper case 9 | /// 10 | /// The name to transform 11 | /// Upper case name 12 | public override string Transform(string name) 13 | { 14 | if (name.IsNullOrEmpty()) return name; 15 | return name.ToUpperInvariant(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/SignalRSwaggerGen.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.OpenApi.Models; 3 | using SignalRSwaggerGen.Attributes; 4 | using SignalRSwaggerGen.Enums; 5 | using SignalRSwaggerGen.Utils; 6 | using SignalRSwaggerGen.Utils.Comparison; 7 | using SignalRSwaggerGen.Utils.XmlComments; 8 | using Swashbuckle.AspNetCore.SwaggerGen; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Linq; 13 | using System.Reflection; 14 | using System.Threading.Tasks; 15 | using System.Xml.Serialization; 16 | 17 | namespace SignalRSwaggerGen 18 | { 19 | internal sealed class SignalRSwaggerGen : IDocumentFilter 20 | { 21 | private static readonly SignalRReturnAttributeComparer _returnAttributeComparer = new SignalRReturnAttributeComparer(); 22 | private readonly SignalRSwaggerGenOptions _options; 23 | private readonly List _xmlComments; 24 | 25 | public SignalRSwaggerGen(SignalRSwaggerGenOptions options) 26 | { 27 | _options = options ?? throw new ArgumentNullException(nameof(options)); 28 | if (_options.Assemblies.Count == 0) _options.ScanAssembly(Assembly.GetEntryAssembly()); 29 | _xmlComments = new List(); 30 | LoadXmlComments(); 31 | } 32 | 33 | public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) 34 | { 35 | var hubs = GetHubs(); 36 | foreach (var hub in hubs) 37 | { 38 | var xmlComments = GetXmlComments(hub); 39 | ProcessHub(swaggerDoc, context, hub, xmlComments); 40 | } 41 | } 42 | 43 | private void ProcessHub(OpenApiDocument swaggerDoc, DocumentFilterContext context, Type hub, XmlComments xmlComments) 44 | { 45 | var hubAttribute = hub.GetCustomAttribute(); 46 | if (!HubShouldBeDisplayedOnDocument(context, hubAttribute)) return; 47 | var hubXml = GetHubXml(hub, xmlComments); 48 | var hubPath = GetHubPath(hub, hubAttribute); 49 | var tag = GetTag(hub, hubAttribute, hubXml); 50 | var methods = GetHubMethods(hub, hubAttribute); 51 | foreach (var method in methods) 52 | { 53 | var methodXml = GetMethodXml(method, xmlComments); 54 | ProcessMethod(swaggerDoc, context, hub, hubAttribute, hubPath, tag, method, methodXml); 55 | } 56 | } 57 | 58 | private void ProcessMethod( 59 | OpenApiDocument swaggerDoc, 60 | DocumentFilterContext context, 61 | Type hub, 62 | SignalRHubAttribute hubAttribute, 63 | string hubPath, 64 | string tag, 65 | MethodInfo method, 66 | MemberElement methodXml) 67 | { 68 | var methodAttribute = method.GetCustomAttribute(); 69 | var methodPath = GetMethodPath(hubPath, method, hubAttribute, methodAttribute); 70 | var methodParams = GetMethodParams(method, hubAttribute, methodAttribute); 71 | var methodReturnParam = method.ReturnParameter; 72 | var operation = GetOperation(methodAttribute); 73 | var summary = GetMethodSummary(hubAttribute, methodAttribute, methodXml); 74 | var description = methodAttribute?.Description; 75 | AddOpenApiPath(swaggerDoc, context, hub, hubAttribute, tag, methodPath, operation, summary, description, methodParams, methodReturnParam, method, methodXml); 76 | } 77 | 78 | private static void AddOpenApiPath( 79 | OpenApiDocument swaggerDoc, 80 | DocumentFilterContext context, 81 | Type hub, 82 | SignalRHubAttribute hubAttribute, 83 | string tag, 84 | string methodPath, 85 | Operation operation, 86 | string summary, 87 | string description, 88 | IEnumerable methodParams, 89 | ParameterInfo methodReturnParam, 90 | MethodInfo method, 91 | MemberElement methodXml) 92 | { 93 | swaggerDoc.Paths.Add( 94 | methodPath, 95 | new OpenApiPathItem 96 | { 97 | Operations = new Dictionary 98 | { 99 | { 100 | (OperationType)operation, 101 | new OpenApiOperation 102 | { 103 | Summary = summary, 104 | Description = description, 105 | Tags = new List { new OpenApiTag { Name = tag } }, 106 | Parameters = ToOpenApiParameters(context, hubAttribute, methodParams, methodXml).ToList(), 107 | Responses = ToOpenApiResponses(context, methodReturnParam), 108 | RequestBody = GetOpenApiRequestBody(context, method), 109 | Security = GetSecurity(hub, method), 110 | } 111 | } 112 | } 113 | }); 114 | } 115 | 116 | private static IList GetSecurity(Type hub, MethodInfo method) 117 | { 118 | var securityEnabled = 119 | hub.GetCustomAttribute() != null 120 | && method.GetCustomAttribute() == null 121 | || method.GetCustomAttribute() != null; 122 | 123 | return securityEnabled 124 | ? new List 125 | { 126 | new OpenApiSecurityRequirement 127 | { 128 | { 129 | new OpenApiSecurityScheme 130 | { 131 | Reference = new OpenApiReference 132 | { 133 | Type = ReferenceType.SecurityScheme, 134 | Id = "basic", 135 | } 136 | }, 137 | Array.Empty() 138 | } 139 | } 140 | } 141 | : null; 142 | } 143 | 144 | private static IEnumerable ToOpenApiParameters( 145 | DocumentFilterContext context, 146 | SignalRHubAttribute hubAttribute, 147 | IEnumerable parameters, 148 | MemberElement methodXml) 149 | { 150 | return parameters.Select(param => 151 | { 152 | var paramXml = methodXml?.Params?.FirstOrDefault(x => x.Name == param.Name); 153 | var paramAttribute = param.GetCustomAttribute(); 154 | var description = GetParamDescription(hubAttribute, paramAttribute, paramXml); 155 | var type = paramAttribute?.ParamType ?? param.ParameterType; 156 | return new OpenApiParameter 157 | { 158 | Name = param.Name, 159 | In = ParameterLocation.Query, 160 | Description = description, 161 | Schema = GetOpenApiSchema(context, type), 162 | }; 163 | }); 164 | } 165 | 166 | private static OpenApiResponses ToOpenApiResponses(DocumentFilterContext context, ParameterInfo returnParam) 167 | { 168 | if (returnParam.GetCustomAttribute() != null) return null; 169 | var responses = new OpenApiResponses(); 170 | var returnAttributes = returnParam.GetCustomAttributes().Distinct(_returnAttributeComparer).ToList(); 171 | if (!returnAttributes.Any()) returnAttributes.Add(new SignalRReturnAttribute()); 172 | foreach (var returnAttribute in returnAttributes) 173 | { 174 | var type = returnAttribute.ReturnType ?? returnParam.ParameterType; 175 | if (!TryGetReturnType(type, out type)) continue; 176 | var mediaType = new OpenApiMediaType 177 | { 178 | Schema = GetOpenApiSchema(context, type) 179 | }; 180 | responses.Add(returnAttribute.StatusCode.ToString(), new OpenApiResponse 181 | { 182 | Description = returnAttribute.Description, 183 | Content = GetContentByMediaType(mediaType) 184 | }); 185 | } 186 | return responses; 187 | } 188 | 189 | private static OpenApiRequestBody GetOpenApiRequestBody(DocumentFilterContext context, MethodInfo method) 190 | { 191 | var requestBodyAttribute = method.GetCustomAttribute(); 192 | if (requestBodyAttribute == null) return null; 193 | var mediaType = new OpenApiMediaType 194 | { 195 | Schema = GetOpenApiSchema(context, requestBodyAttribute.BodyType) 196 | }; 197 | return new OpenApiRequestBody 198 | { 199 | Required = requestBodyAttribute.IsRequired, 200 | Description = requestBodyAttribute.Description, 201 | Content = GetContentByMediaType(mediaType) 202 | }; 203 | } 204 | 205 | private static OpenApiSchema GetOpenApiSchema(DocumentFilterContext context, Type type) 206 | { 207 | if (!context.SchemaRepository.TryLookupByType(type, out OpenApiSchema schema)) 208 | { 209 | schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); 210 | } 211 | return schema.Reference == null 212 | ? schema 213 | : new OpenApiSchema 214 | { 215 | Reference = new OpenApiReference 216 | { 217 | Id = schema.Reference.Id, 218 | Type = ReferenceType.Schema 219 | } 220 | }; 221 | } 222 | 223 | private static Dictionary GetContentByMediaType(OpenApiMediaType mediaType) 224 | { 225 | return new Dictionary 226 | { 227 | { "application/json", mediaType }, 228 | { "text/json", mediaType }, 229 | { "text/plain", mediaType }, 230 | }; 231 | } 232 | 233 | private bool HubShouldBeDisplayedOnDocument(DocumentFilterContext context, SignalRHubAttribute hubAttribute) 234 | { 235 | var documentNames = hubAttribute.DocumentNames ?? _options.DocumentNames; 236 | return !documentNames.Any() 237 | || documentNames.Contains(context.DocumentName); 238 | } 239 | 240 | private string GetHubPath(Type hub, SignalRHubAttribute hubAttribute) 241 | { 242 | var hubName = GetHubName(hub); 243 | var nameTransformer = hubAttribute.NameTransformer ?? _options.NameTransformer; 244 | if (nameTransformer != null) hubName = nameTransformer.Transform(hubName); 245 | if (hubAttribute.Path != null) return hubAttribute.Path.Replace(Constants.HubNamePlaceholder, hubName); 246 | return _options.HubPathFunc(hubName); 247 | } 248 | 249 | private static string GetHubName(Type hub) 250 | { 251 | var hubName = hub.IsInterface && hub.Name[0] == 'I' 252 | ? hub.Name.Substring(1) 253 | : hub.Name; 254 | return hubName.Split('`')[0]; 255 | } 256 | 257 | private string GetTag(Type hub, SignalRHubAttribute hubAttribute, MemberElement hubXml) 258 | { 259 | if (hubAttribute.Tag != null) return hubAttribute.Tag; 260 | if (!hubAttribute.XmlCommentsDisabled 261 | && _options.UseHubXmlCommentsSummaryAsTag 262 | && hubXml?.Summary?.Text != null) return hubXml.Summary.Text; 263 | return GetHubName(hub); 264 | } 265 | 266 | private string GetMethodPath(string hubPath, MethodInfo method, SignalRHubAttribute hubAttribute, SignalRMethodAttribute methodAttribute) 267 | { 268 | var methodPathSuffix = new string(' ', method.GetParameters().Length); 269 | var methodName = methodAttribute == null 270 | ? method.Name 271 | : methodAttribute.Name.Replace(Constants.MethodNamePlaceholder, method.Name); 272 | var nameTransformer = hubAttribute.NameTransformer ?? _options.NameTransformer; 273 | if (nameTransformer != null) methodName = nameTransformer.Transform(methodName); 274 | return $"{hubPath}/{methodName}{methodPathSuffix}"; 275 | } 276 | 277 | private Operation GetOperation(SignalRMethodAttribute methodAttribute) 278 | { 279 | var operation = methodAttribute?.Operation ?? Operation.Inherit; 280 | if (operation == Operation.Inherit) operation = _options.Operation; 281 | return operation; 282 | } 283 | 284 | private IEnumerable GetHubs() 285 | { 286 | return _options.Assemblies 287 | .SelectMany(a => 288 | a.GetTypes() 289 | .Where(t => 290 | t.GetCustomAttribute() != null 291 | && t.GetCustomAttribute() == null)); 292 | } 293 | 294 | private IEnumerable GetHubMethods(Type hub, SignalRHubAttribute hubAttribute) 295 | { 296 | var autoDiscover = GetAutoDiscover(hubAttribute); 297 | IEnumerable methods; 298 | switch (autoDiscover) 299 | { 300 | case AutoDiscover.None: 301 | case AutoDiscover.Params: 302 | methods = hub.GetMethods(ReflectionUtils.DeclaredPublicInstance).Where(x => x.GetCustomAttribute() != null); 303 | break; 304 | case AutoDiscover.Methods: 305 | case AutoDiscover.MethodsAndParams: 306 | methods = hub.GetMethods(ReflectionUtils.DeclaredPublicInstance); 307 | break; 308 | default: 309 | throw new NotSupportedException($"Auto-discover option '{autoDiscover}' not supported"); 310 | } 311 | return methods.Where(x => x.GetCustomAttribute() == null); 312 | } 313 | 314 | private IEnumerable GetMethodParams(MethodInfo method, SignalRHubAttribute hubAttribute, SignalRMethodAttribute methodAttribute) 315 | { 316 | var autoDiscover = GetAutoDiscover(hubAttribute, methodAttribute); 317 | IEnumerable methodParams; 318 | switch (autoDiscover) 319 | { 320 | case AutoDiscover.None: 321 | case AutoDiscover.Methods: 322 | methodParams = method.GetParameters().Where(x => x.GetCustomAttribute() != null); 323 | break; 324 | case AutoDiscover.Params: 325 | case AutoDiscover.MethodsAndParams: 326 | methodParams = method.GetParameters(); 327 | break; 328 | default: 329 | throw new NotSupportedException($"Auto-discover option '{autoDiscover}' not supported"); 330 | } 331 | return methodParams.Where(x => x.GetCustomAttribute() == null); 332 | } 333 | 334 | private AutoDiscover GetAutoDiscover(SignalRHubAttribute hubAttribute) 335 | { 336 | var autoDiscover = hubAttribute.AutoDiscover; 337 | if (autoDiscover == AutoDiscover.Inherit) autoDiscover = _options.AutoDiscover; 338 | return autoDiscover; 339 | } 340 | 341 | private AutoDiscover GetAutoDiscover(SignalRHubAttribute hubAttribute, SignalRMethodAttribute methodAttribute) 342 | { 343 | var autoDiscover = methodAttribute?.AutoDiscover ?? AutoDiscover.Inherit; 344 | if (autoDiscover == AutoDiscover.Inherit) autoDiscover = hubAttribute.AutoDiscover; 345 | if (autoDiscover == AutoDiscover.Inherit) autoDiscover = _options.AutoDiscover; 346 | return autoDiscover; 347 | } 348 | 349 | private static string GetMethodSummary(SignalRHubAttribute hubAttribute, SignalRMethodAttribute methodAttribute, MemberElement methodXml) 350 | { 351 | var summary = methodAttribute?.Summary; 352 | if (summary != null) return summary; 353 | if (!hubAttribute.XmlCommentsDisabled) summary = methodXml?.Summary?.Text; 354 | return summary; 355 | } 356 | 357 | private static string GetParamDescription(SignalRHubAttribute hubAttribute, SignalRParamAttribute paramAttribute, ParamElement paramXml) 358 | { 359 | var description = paramAttribute?.Description; 360 | if (description != null) return description; 361 | if (!hubAttribute.XmlCommentsDisabled) description = paramXml?.Text; 362 | return description; 363 | } 364 | 365 | private XmlComments GetXmlComments(Type hub) 366 | { 367 | return _xmlComments.FirstOrDefault(x => x.Assembly?.Name?.Text == hub.Assembly.GetName().Name); 368 | } 369 | 370 | private static MemberElement GetHubXml(Type hub, XmlComments xmlComments) 371 | { 372 | return xmlComments?.Members?.FirstOrDefault(x => x.Name == hub.GetXmlCommentsName()); 373 | } 374 | 375 | private static MemberElement GetMethodXml(MethodInfo method, XmlComments xmlComments) 376 | { 377 | return xmlComments?.Members?.FirstOrDefault(x => x.Name == method.GetXmlCommentsName()); 378 | } 379 | 380 | private static bool TryGetReturnType(Type inType, out Type outType) 381 | { 382 | outType = inType; 383 | if (inType.IsGenericType) 384 | { 385 | if (inType.IsGenericTypeDefinition) return false; 386 | var genericTypeDef = inType.GetGenericTypeDefinition(); 387 | if (genericTypeDef == typeof(Task<>) 388 | || genericTypeDef == typeof(ValueTask<>)) 389 | { 390 | outType = inType.GetGenericArguments()[0]; 391 | return true; 392 | } 393 | } 394 | else 395 | { 396 | if (inType == typeof(void) 397 | || inType == typeof(Task) 398 | || inType == typeof(ValueTask)) return false; 399 | } 400 | return true; 401 | } 402 | 403 | private void LoadXmlComments() 404 | { 405 | var xmlSerializer = new XmlSerializer(typeof(XmlComments)); 406 | foreach (var path in _options.PathsToXmlCommentsFiles) 407 | { 408 | using (var streamReader = new StreamReader(path)) 409 | { 410 | var xmlComments = (XmlComments)xmlSerializer.Deserialize(streamReader); 411 | _xmlComments.Add(xmlComments); 412 | } 413 | } 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/SignalRSwaggerGen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | false 6 | true 7 | Dorin Mocan 8 | Essencebit 9 | This package can be used to generate Swagger documentation for SignalR hubs. 10 | swagger openapi open api swashbuckle swaggergen documentation signalr hub websocket wss rest api web socket http doc generation extension handler handle intercept plugin auto standard net core microsoft web socket schema ui gen extension 11 | https://github.com/Dorin-Mocan/SignalRSwaggerGen 12 | https://github.com/Dorin-Mocan/SignalRSwaggerGen 13 | 3.2.1 14 | Fixed default hub path template 15 | 3.2.1.0 16 | false 17 | 3.2.1.0 18 | LICENSE.md 19 | Logo.png 20 | 21 | 22 | 23 | 24 | C:\Projects\SignalRSwaggerGen\SignalRSwaggerGen\SignalRSwaggerGen\SignalRSwaggerGen.xml 25 | 26 | 27 | 28 | 29 | True 30 | 31 | 32 | 33 | True 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/SignalRSwaggerGen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SignalRSwaggerGen 5 | 6 | 7 | 8 | 9 | Use this attribute to disable Swagger documentation for specific components 10 | 11 | 12 | 13 | 14 | Use this attribute to enable Swagger documentation for hubs 15 | 16 | 17 | 18 | Path of the hub. If path contains "[Hub]", this part will be replaced with the name of the type holding this attribute(hub name). 19 | If not specified, the func from 'SignalRSwaggerGenOptions' will be used to get the path. 20 | A flag indicating what components will have Swagger documentation enabled automatically. 21 | If 'AutoDiscover.Inherit' specified, the value from 'SignalRSwaggerGenOptions' will be used. 22 | An array of names of the Swagger documents the hub will be displayed in. 23 | If null specified, then the value from 'SignalRSwaggerGenOptions' will be used. If empty array specified, then the hub will be displayed in all documents. 24 | The type of the name transformer. The type must inherit from 'SignalRSwaggerGen.Naming.NameTransformer' class, be non-abstract and have public parameterless constructor. 25 | The name transformer will be used to transform the name of the hub and its methods. If null specified, the transformer from 'SignalRSwaggerGenOptions' will be used. 26 | The namespace 'SignalRSwaggerGen.Naming' already contains some predefined name transformers, so check 'em out. 27 | The tag under which the hub will be placed in Swagger doc. If null specified, the summary section of the XML comments of the hub will be used. 28 | If XML comments missing or not enabled, the name of the type holding this attribute will be used. 29 | A flag indicating if XML comments are disabled for the hub 30 | Thrown if 31 | - value not allowed for this attribute 32 | - is abstract or does not inherit from 'SignalRSwaggerGen.Naming.NameTransformer' class or has no public parameterless constructor 33 | 34 | 35 | 36 | Use this attribute to enable Swagger documentation for hub methods 37 | 38 | 39 | 40 | Name of the method which will be invoked on the client side. 41 | If name contains "[Method]", this part will be replaced with the name of the method holding this attribute. 42 | Same as HTTP verb. If 'Operation.Inherit' specified, then the value from 'SignalRSwaggerGenOptions' will be used. 43 | A flag indicating what components will have Swagger documentation enabled automatically. 44 | If 'AutoDiscover.Inherit' specified, the value from the hub will be used. If the hub also has this value specified, then the value from 'SignalRSwaggerGenOptions' will be used. 45 | The text that will appear in summary section of the decorated method in Swagger document. 46 | If null specified and XML comments not disabled, the summary section of the XML comments of the method will be used. 47 | The text that will appear in description section of decorated method in Swagger document 48 | Thrown if 49 | - is null or empty 50 | - value not allowed for this attribute 51 | 52 | 53 | 54 | Use this attribute to enable Swagger documentation for method params 55 | 56 | 57 | 58 | The text that will appear in description section of decorated parameter in Swagger document 59 | Parameter type. If null specified, the type of the parameter holding this attribute will be picked up. 60 | 61 | 62 | 63 | Use this attribute to enable Swagger documentation for request body 64 | 65 | 66 | 67 | The type of the request body 68 | The value that indicates if the request body is required or not 69 | The text that will appear in description section of the corresponding request body in Swagger document 70 | Thrown if is null 71 | 72 | 73 | 74 | Use this attribute to enable Swagger documentation for method return type 75 | 76 | 77 | 78 | Return type. If null specified, the return type of the method will be picked up. 79 | The text that will appear in status code section of the corresponding response in Swagger document 80 | The text that will appear in description section of the corresponding response in Swagger document 81 | 82 | 83 | 84 | A flag indicating what components will have Swagger documentation enabled automatically 85 | 86 | 87 | 88 | 89 | Inherit value from higher level configurations 90 | 91 | 92 | 93 | 94 | None 95 | 96 | 97 | 98 | 99 | Public non-static methods 100 | 101 | 102 | 103 | 104 | Method params 105 | 106 | 107 | 108 | 109 | Public non-static methods and their params 110 | 111 | 112 | 113 | 114 | Same as HTTP verb 115 | 116 | 117 | 118 | 119 | Inherit value from higher level configurations 120 | 121 | 122 | 123 | 124 | Inherit from this class in order to create a name transformer 125 | 126 | 127 | 128 | 129 | Name transformation function 130 | 131 | The name to transform 132 | Transformed name 133 | 134 | 135 | 136 | Transforms the name to camel case 137 | 138 | The name to transform 139 | Camel case name 140 | 141 | 142 | 143 | Transforms the name to lower case 144 | 145 | The name to transform 146 | Lower case name 147 | 148 | 149 | 150 | Transforms the name to upper case 151 | 152 | The name to transform 153 | Upper case name 154 | 155 | 156 | 157 | Options used by SignalRSwaggerGen to generate documentation for SignalR hubs 158 | 159 | 160 | 161 | 162 | The func that will get the hub name and return the path for the hub. The func will be skipped for the hubs that have a not null path specified for them in particular. 163 | If the func not specified explicitly, the default func will return a value based on the template 'Constants.DefaultHubPathTemplate'. 164 | If you decide to set a custom func, make sure the func will return a different path for each hub. 165 | 166 | hubName => $"hubs/are/here/{hubName}" 167 | Thrown if the value is null 168 | 169 | 170 | 171 | A flag indicating what components will have Swagger documentation enabled automatically. 172 | Can be overridden for a specific component by specifying auto-discover value for that component in particular. 173 | If not specified explicitly, the default value is 'Constants.DefaultAutoDiscover'. 174 | 175 | Thrown if the value is 'AutoDiscover.Inherit', since there's no other higher level configuration to inherit from 176 | 177 | 178 | 179 | Same as HTTP verb. Can be overridden for a specific method by specifying the operation for that method in particular. 180 | If not specified explicitly, the default value is 'Constants.DefaultOperation'. 181 | 182 | Thrown if the value is 'Operation.Inherit', since there's no other higher level configuration to inherit from 183 | 184 | 185 | 186 | The name transformer that will be used to transform the name of the hubs and their methods. 187 | Can be overridden for a specific component by specifying a transformer for that component in particular. 188 | If not specified at any level, no transformation will happen. The namespace 'SignalRSwaggerGen.Naming' already contains some predefined name transformers, so check 'em out. 189 | 190 | Thrown if the value is null 191 | 192 | 193 | 194 | Use summary section from hub's XML comments as tag for Swagger doc 195 | 196 | 197 | 198 | 199 | Specify the assembly to be scanned for SignalR hubs. If no assemblies specified explicitly, the entry assembly will be scanned by default. 200 | This method has additive effect. You can use it multiple times to add more assemblies. 201 | 202 | Assembly to be scanned for SignalR hubs 203 | Thrown if is null 204 | 205 | 206 | 207 | Specify assemblies to be scanned for SignalR hubs. If no assemblies specified explicitly, the entry assembly will be scanned by default. 208 | This method has additive effect. You can use it multiple times to add more assemblies. 209 | 210 | Assemblies to be scanned for SignalR hubs 211 | Thrown if or any of its items is null 212 | Thrown if is empty 213 | 214 | 215 | 216 | Specify assemblies to be scanned for SignalR hubs. If no assemblies specified explicitly, the entry assembly will be scanned by default. 217 | This method has additive effect. You can use it multiple times to add more assemblies. 218 | 219 | Assemblies to be scanned for SignalR hubs 220 | Thrown if or any of its items is null 221 | Thrown if is empty 222 | 223 | 224 | 225 | Specify the name of the Swagger document the hubs will be displayed in. 226 | Can be overridden for a specific hub by specifying document names for that hub in particular. 227 | If no document names specified explicitly, then the hubs will be displayed in all documents. 228 | This method has additive effect. You can use it multiple times to add more document names. 229 | 230 | Name of the Swagger document the hubs will be displayed in 231 | Thrown if is null 232 | 233 | 234 | 235 | Specify the list of names of the Swagger documents the hubs will be displayed in. 236 | Can be overridden for a specific hub by specifying document names for that hub in particular. 237 | If no document names specified explicitly, then the hubs will be displayed in all documents. 238 | This method has additive effect. You can use it multiple times to add more document names. 239 | 240 | Names of the Swagger documents the hubs will be displayed in 241 | Thrown if or any of its items is null 242 | Thrown if is empty 243 | 244 | 245 | 246 | Specify the list of names of the Swagger documents the hubs will be displayed in. 247 | Can be overridden for a specific hub by specifying document names for that hub in particular. 248 | If no document names specified explicitly, then the hubs will be displayed in all documents. 249 | This method has additive effect. You can use it multiple times to add more document names. 250 | 251 | Names of the Swagger documents the hubs will be displayed in 252 | Thrown if or any of its items is null 253 | Thrown if is empty 254 | 255 | 256 | 257 | Specify an XML comments file to be used for generating Swagger doc. 258 | This method has additive effect. You can use it multiple times to add more XML comments files. 259 | 260 | Path to the file that contains XML comments 261 | Thrown if is null 262 | Thrown if does not exist 263 | 264 | 265 | 266 | Specify a list of XML comments files to be used for generating Swagger doc. 267 | This method has additive effect. You can use it multiple times to add more XML comments files. 268 | 269 | Paths to the files that contain XML comments 270 | Thrown if or any of its items is null 271 | Thrown if is empty or any of its items does not exist 272 | 273 | 274 | 275 | Specify a list of XML comments files to be used for generating Swagger doc. 276 | This method has additive effect. You can use it multiple times to add more XML comments files. 277 | 278 | Paths to the files that contain XML comments 279 | Thrown if or any of its items is null 280 | Thrown if is empty or any of its items does not exist 281 | 282 | 283 | 284 | Add SignalRSwaggerGen to generate documentation for SignalR hubs 285 | 286 | ... 287 | 288 | 289 | 290 | Add SignalRSwaggerGen to generate documentation for SignalR hubs 291 | 292 | ... 293 | Action for setting up options for SignalRSwaggerGen 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/SignalRSwaggerGenOptions.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Enums; 2 | using SignalRSwaggerGen.Naming; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | namespace SignalRSwaggerGen 10 | { 11 | /// 12 | /// Options used by SignalRSwaggerGen to generate documentation for SignalR hubs 13 | /// 14 | public class SignalRSwaggerGenOptions 15 | { 16 | /// 17 | /// The func that will get the hub name and return the path for the hub. The func will be skipped for the hubs that have a not null path specified for them in particular. 18 | /// If the func not specified explicitly, the default func will return a value based on the template 'Constants.DefaultHubPathTemplate'. 19 | /// If you decide to set a custom func, make sure the func will return a different path for each hub. 20 | /// 21 | /// hubName => $"hubs/are/here/{hubName}" 22 | /// Thrown if the value is null 23 | public Func HubPathFunc 24 | { 25 | get => _hubPathFunc; 26 | set 27 | { 28 | _hubPathFunc = value ?? throw new ArgumentNullException(nameof(HubPathFunc)); 29 | } 30 | } 31 | 32 | /// 33 | /// A flag indicating what components will have Swagger documentation enabled automatically. 34 | /// Can be overridden for a specific component by specifying auto-discover value for that component in particular. 35 | /// If not specified explicitly, the default value is 'Constants.DefaultAutoDiscover'. 36 | /// 37 | /// Thrown if the value is 'AutoDiscover.Inherit', since there's no other higher level configuration to inherit from 38 | public AutoDiscover AutoDiscover 39 | { 40 | get => _autoDiscover; 41 | set 42 | { 43 | if (value == AutoDiscover.Inherit) throw new ArgumentException($"Auto-discover option '{value}' not allowed, since there's no other higher level configuration to inherit from"); 44 | _autoDiscover = value; 45 | } 46 | } 47 | 48 | /// 49 | /// Same as HTTP verb. Can be overridden for a specific method by specifying the operation for that method in particular. 50 | /// If not specified explicitly, the default value is 'Constants.DefaultOperation'. 51 | /// 52 | /// Thrown if the value is 'Operation.Inherit', since there's no other higher level configuration to inherit from 53 | public Operation Operation 54 | { 55 | get => _operation; 56 | set 57 | { 58 | if (value == Operation.Inherit) throw new ArgumentException($"Operation '{value}' not allowed, since there's no other higher level configuration to inherit from"); 59 | _operation = value; 60 | } 61 | } 62 | 63 | /// 64 | /// The name transformer that will be used to transform the name of the hubs and their methods. 65 | /// Can be overridden for a specific component by specifying a transformer for that component in particular. 66 | /// If not specified at any level, no transformation will happen. The namespace 'SignalRSwaggerGen.Naming' already contains some predefined name transformers, so check 'em out. 67 | /// 68 | /// Thrown if the value is null 69 | public NameTransformer NameTransformer 70 | { 71 | get => _nameTransformer; 72 | set 73 | { 74 | _nameTransformer = value ?? throw new ArgumentNullException(nameof(NameTransformer)); 75 | } 76 | } 77 | 78 | /// 79 | /// Use summary section from hub's XML comments as tag for Swagger doc 80 | /// 81 | public bool UseHubXmlCommentsSummaryAsTag { get; set; } 82 | 83 | /// 84 | /// Specify the assembly to be scanned for SignalR hubs. If no assemblies specified explicitly, the entry assembly will be scanned by default. 85 | /// This method has additive effect. You can use it multiple times to add more assemblies. 86 | /// 87 | /// Assembly to be scanned for SignalR hubs 88 | /// Thrown if is null 89 | public void ScanAssembly(Assembly assembly) 90 | { 91 | if (assembly == null) throw new ArgumentNullException(nameof(assembly)); 92 | Assemblies.Add(assembly); 93 | } 94 | 95 | /// 96 | /// Specify assemblies to be scanned for SignalR hubs. If no assemblies specified explicitly, the entry assembly will be scanned by default. 97 | /// This method has additive effect. You can use it multiple times to add more assemblies. 98 | /// 99 | /// Assemblies to be scanned for SignalR hubs 100 | /// Thrown if or any of its items is null 101 | /// Thrown if is empty 102 | public void ScanAssemblies(IEnumerable assemblies) 103 | { 104 | if (assemblies == null) throw new ArgumentNullException(nameof(assemblies)); 105 | if (!assemblies.Any()) throw new ArgumentException("Empty", nameof(assemblies)); 106 | foreach (var assembly in assemblies) 107 | { 108 | ScanAssembly(assembly); 109 | } 110 | } 111 | 112 | /// 113 | /// Specify assemblies to be scanned for SignalR hubs. If no assemblies specified explicitly, the entry assembly will be scanned by default. 114 | /// This method has additive effect. You can use it multiple times to add more assemblies. 115 | /// 116 | /// Assemblies to be scanned for SignalR hubs 117 | /// Thrown if or any of its items is null 118 | /// Thrown if is empty 119 | public void ScanAssemblies(params Assembly[] assemblies) 120 | { 121 | if (assemblies == null) throw new ArgumentNullException(nameof(assemblies)); 122 | if (assemblies.Length == 0) throw new ArgumentException("Empty", nameof(assemblies)); 123 | foreach (var assembly in assemblies) 124 | { 125 | ScanAssembly(assembly); 126 | } 127 | } 128 | 129 | /// 130 | /// Specify the name of the Swagger document the hubs will be displayed in. 131 | /// Can be overridden for a specific hub by specifying document names for that hub in particular. 132 | /// If no document names specified explicitly, then the hubs will be displayed in all documents. 133 | /// This method has additive effect. You can use it multiple times to add more document names. 134 | /// 135 | /// Name of the Swagger document the hubs will be displayed in 136 | /// Thrown if is null 137 | public void DisplayInDocument(string documentName) 138 | { 139 | if (documentName == null) throw new ArgumentNullException(nameof(documentName)); 140 | DocumentNames.Add(documentName); 141 | } 142 | 143 | /// 144 | /// Specify the list of names of the Swagger documents the hubs will be displayed in. 145 | /// Can be overridden for a specific hub by specifying document names for that hub in particular. 146 | /// If no document names specified explicitly, then the hubs will be displayed in all documents. 147 | /// This method has additive effect. You can use it multiple times to add more document names. 148 | /// 149 | /// Names of the Swagger documents the hubs will be displayed in 150 | /// Thrown if or any of its items is null 151 | /// Thrown if is empty 152 | public void DisplayInDocuments(IEnumerable documentNames) 153 | { 154 | if (documentNames == null) throw new ArgumentNullException(nameof(documentNames)); 155 | if (!documentNames.Any()) throw new ArgumentException("Empty", nameof(documentNames)); 156 | foreach (var documentName in documentNames) 157 | { 158 | DisplayInDocument(documentName); 159 | } 160 | } 161 | 162 | /// 163 | /// Specify the list of names of the Swagger documents the hubs will be displayed in. 164 | /// Can be overridden for a specific hub by specifying document names for that hub in particular. 165 | /// If no document names specified explicitly, then the hubs will be displayed in all documents. 166 | /// This method has additive effect. You can use it multiple times to add more document names. 167 | /// 168 | /// Names of the Swagger documents the hubs will be displayed in 169 | /// Thrown if or any of its items is null 170 | /// Thrown if is empty 171 | public void DisplayInDocuments(params string[] documentNames) 172 | { 173 | if (documentNames == null) throw new ArgumentNullException(nameof(documentNames)); 174 | if (documentNames.Length == 0) throw new ArgumentException("Empty", nameof(documentNames)); 175 | foreach (var documentName in documentNames) 176 | { 177 | DisplayInDocument(documentName); 178 | } 179 | } 180 | 181 | /// 182 | /// Specify an XML comments file to be used for generating Swagger doc. 183 | /// This method has additive effect. You can use it multiple times to add more XML comments files. 184 | /// 185 | /// Path to the file that contains XML comments 186 | /// Thrown if is null 187 | /// Thrown if does not exist 188 | public void UseXmlComments(string path) 189 | { 190 | if (path == null) throw new ArgumentNullException(nameof(path)); 191 | if (!File.Exists(path)) throw new ArgumentException($"Does not exist: path=[{path}]", nameof(path)); 192 | PathsToXmlCommentsFiles.Add(path); 193 | } 194 | 195 | /// 196 | /// Specify a list of XML comments files to be used for generating Swagger doc. 197 | /// This method has additive effect. You can use it multiple times to add more XML comments files. 198 | /// 199 | /// Paths to the files that contain XML comments 200 | /// Thrown if or any of its items is null 201 | /// Thrown if is empty or any of its items does not exist 202 | public void UseXmlComments(IEnumerable paths) 203 | { 204 | if (paths == null) throw new ArgumentNullException(nameof(paths)); 205 | if (!paths.Any()) throw new ArgumentException("Empty", nameof(paths)); 206 | foreach (var path in paths) 207 | { 208 | UseXmlComments(path); 209 | } 210 | } 211 | 212 | /// 213 | /// Specify a list of XML comments files to be used for generating Swagger doc. 214 | /// This method has additive effect. You can use it multiple times to add more XML comments files. 215 | /// 216 | /// Paths to the files that contain XML comments 217 | /// Thrown if or any of its items is null 218 | /// Thrown if is empty or any of its items does not exist 219 | public void UseXmlComments(params string[] paths) 220 | { 221 | if (paths == null) throw new ArgumentNullException(nameof(paths)); 222 | if (paths.Length == 0) throw new ArgumentException("Empty", nameof(paths)); 223 | foreach (var path in paths) 224 | { 225 | UseXmlComments(path); 226 | } 227 | } 228 | 229 | internal HashSet Assemblies { get; } = new HashSet(); 230 | internal HashSet PathsToXmlCommentsFiles { get; } = new HashSet(); 231 | internal HashSet DocumentNames { get; } = new HashSet(); 232 | 233 | private Func _hubPathFunc = hubName => Constants.DefaultHubPathTemplate.Replace(Constants.HubNamePlaceholder, hubName); 234 | private AutoDiscover _autoDiscover = Constants.DefaultAutoDiscover; 235 | private Operation _operation = Constants.DefaultOperation; 236 | private NameTransformer _nameTransformer; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/SwaggerGenOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen; 2 | using Swashbuckle.AspNetCore.SwaggerGen; 3 | using System; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class SwaggerGenOptionsExtensions 8 | { 9 | /// 10 | /// Add SignalRSwaggerGen to generate documentation for SignalR hubs 11 | /// 12 | /// ... 13 | public static void AddSignalRSwaggerGen(this SwaggerGenOptions swaggerGenOptions) 14 | { 15 | var signalRSwaggerGenOptions = new SignalRSwaggerGenOptions(); 16 | swaggerGenOptions.DocumentFilter(signalRSwaggerGenOptions); 17 | } 18 | 19 | /// 20 | /// Add SignalRSwaggerGen to generate documentation for SignalR hubs 21 | /// 22 | /// ... 23 | /// Action for setting up options for SignalRSwaggerGen 24 | public static void AddSignalRSwaggerGen(this SwaggerGenOptions swaggerGenOptions, Action action) 25 | { 26 | if (action == null) throw new ArgumentNullException(nameof(action)); 27 | var signalRSwaggerGenOptions = new SignalRSwaggerGenOptions(); 28 | action(signalRSwaggerGenOptions); 29 | swaggerGenOptions.DocumentFilter(signalRSwaggerGenOptions); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Utils/Comparison/SignalRReturnAttributeComparer.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Attributes; 2 | using System.Collections.Generic; 3 | 4 | namespace SignalRSwaggerGen.Utils.Comparison 5 | { 6 | internal class SignalRReturnAttributeComparer : IEqualityComparer 7 | { 8 | public bool Equals(SignalRReturnAttribute x, SignalRReturnAttribute y) 9 | { 10 | return x != null 11 | && y != null 12 | && x.StatusCode == y.StatusCode; 13 | } 14 | 15 | public int GetHashCode(SignalRReturnAttribute obj) 16 | { 17 | return obj.StatusCode; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Utils/EnumerableUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace SignalRSwaggerGen.Utils 6 | { 7 | internal static class EnumerableUtils 8 | { 9 | public static IEnumerable TakeLast(this IEnumerable enumerable, int takeCount) 10 | { 11 | if (takeCount < 0) throw new ArgumentException("Less than zero", nameof(takeCount)); 12 | if (enumerable == null) return null; 13 | var enumerableCount = enumerable.Count(); 14 | var skipCount = enumerableCount <= takeCount ? 0 : enumerableCount - takeCount; 15 | return enumerable.Skip(skipCount).Take(takeCount); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Utils/ReflectionUtils.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace SignalRSwaggerGen.Utils 4 | { 5 | internal class ReflectionUtils 6 | { 7 | public const BindingFlags DeclaredPublicInstance = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Utils/StringUtils.cs: -------------------------------------------------------------------------------- 1 | namespace SignalRSwaggerGen.Utils 2 | { 3 | internal static class StringUtils 4 | { 5 | public static bool IsNullOrEmpty(this string s) => string.IsNullOrEmpty(s); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Utils/XmlComments/XmlComments.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Xml.Serialization; 3 | 4 | namespace SignalRSwaggerGen.Utils.XmlComments 5 | { 6 | [XmlRoot("doc")] 7 | public class XmlComments 8 | { 9 | [XmlElement("assembly")] 10 | public AssemblyElement Assembly { get; set; } 11 | 12 | [XmlArray("members")] 13 | [XmlArrayItem("member")] 14 | public List Members { get; set; } = new List(); 15 | } 16 | 17 | public class AssemblyElement 18 | { 19 | [XmlElement("name")] 20 | public AssemblyNameElement Name { get; set; } 21 | } 22 | 23 | public class AssemblyNameElement 24 | { 25 | [XmlText] 26 | public string Text { get; set; } 27 | } 28 | 29 | public class MemberElement 30 | { 31 | [XmlAttribute("name")] 32 | public string Name { get; set; } 33 | 34 | [XmlElement("summary")] 35 | public MemberSummaryElement Summary { get; set; } 36 | 37 | [XmlElement("param")] 38 | public List Params { get; set; } = new List(); 39 | } 40 | 41 | public class MemberSummaryElement 42 | { 43 | [XmlText] 44 | public string Text { get; set; } 45 | } 46 | 47 | public class ParamElement 48 | { 49 | [XmlAttribute("name")] 50 | public string Name { get; set; } 51 | 52 | [XmlText] 53 | public string Text { get; set; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/SignalRSwaggerGen/Utils/XmlComments/XmlCommentsUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace SignalRSwaggerGen.Utils.XmlComments 8 | { 9 | internal static class XmlCommentsUtils 10 | { 11 | public static string GetXmlCommentsName(this Type type) 12 | { 13 | if (type == null) throw new ArgumentNullException(nameof(type)); 14 | 15 | var result = $"T:{GetTypeName(type)}"; 16 | 17 | return result; 18 | } 19 | 20 | public static string GetXmlCommentsName(this MethodInfo method) 21 | { 22 | if (method == null) throw new ArgumentNullException(nameof(method)); 23 | 24 | var result = $"M:{GetTypeName(method.DeclaringType)}.{method.Name}"; 25 | 26 | if (method.IsGenericMethodDefinition) 27 | { 28 | result += $"``{method.GetGenericArguments().Length}"; 29 | } 30 | 31 | var parameters = method.GetParameters(); 32 | if (parameters.Length == 0) return result; 33 | 34 | result += "("; 35 | for (int i = 0; i < parameters.Length; i++) 36 | { 37 | if (i > 0) result += ","; 38 | result += GetParamTypeName(parameters[i].ParameterType); 39 | } 40 | result += ")"; 41 | 42 | return result; 43 | } 44 | 45 | private static string GetTypeName(Type type) 46 | { 47 | return Regex.Replace(type.FullName ?? $"{type.Namespace}.{type.Name}", @"\[.*\]", string.Empty).Replace('+', '.'); 48 | } 49 | 50 | private static string GetParamTypeName(Type paramType) 51 | { 52 | if (paramType.HasElementType) return GetWithElementParamTypeName(paramType); 53 | if (paramType.IsGenericParameter) return GetGenericParameterParamTypeName(paramType); 54 | if (paramType.IsGenericType) return GetGenericParamTypeName(paramType); 55 | return GetSimpleParamTypeName(paramType); 56 | } 57 | 58 | private static string GetWithElementParamTypeName(Type paramType) 59 | { 60 | if (paramType.IsByRef) return GetByRefParamTypeName(paramType); 61 | if (paramType.IsPointer) return GetPointerParamTypeName(paramType); 62 | if (paramType.IsArray) return GetArrayParamTypeName(paramType); 63 | throw new NotSupportedException($"Type [{paramType.AssemblyQualifiedName}] not supported"); 64 | } 65 | 66 | private static string GetByRefParamTypeName(Type paramType) 67 | { 68 | return GetParamTypeName(paramType.GetElementType()) + "@"; 69 | } 70 | 71 | private static string GetPointerParamTypeName(Type paramType) 72 | { 73 | return GetParamTypeName(paramType.GetElementType()) + "*"; 74 | } 75 | 76 | private static string GetArrayParamTypeName(Type paramType) 77 | { 78 | var arrayRank = paramType.GetArrayRank(); 79 | var arrayTypeSpecifier = arrayRank == 1 80 | ? "[]" 81 | : $"[{string.Join(",", Enumerable.Range(0, arrayRank).Select(x => "0:"))}]"; 82 | 83 | return $"{GetParamTypeName(paramType.GetElementType())}{arrayTypeSpecifier}"; 84 | } 85 | 86 | private static string GetGenericParameterParamTypeName(Type paramType) 87 | { 88 | #if NETSTANDARD2_1 89 | if (paramType.IsGenericMethodParameter) return $"``{paramType.GenericParameterPosition}"; 90 | if (paramType.IsGenericTypeParameter) return $"`{paramType.GenericParameterPosition}"; 91 | throw new NotSupportedException($"Type [{paramType.AssemblyQualifiedName}] not supported"); 92 | #else 93 | throw new NotSupportedException("Certain API required for further execution is missing. Target netstandard2.1 to get rid of this exception."); 94 | #endif 95 | } 96 | 97 | private static string GetGenericParamTypeName(Type paramType) 98 | { 99 | var genericArgs = paramType.GetGenericArguments(); 100 | if (paramType.IsNested) return GetNestedGenericParamTypeName(paramType, genericArgs); 101 | return $"{(paramType.FullName ?? $"{paramType.Namespace}.{paramType.Name}").Split('`')[0]}{{{string.Join(",", genericArgs.Select(x => GetParamTypeName(x)))}}}"; 102 | } 103 | 104 | private static string GetNestedGenericParamTypeName(Type paramType, IEnumerable allGenericArgs) 105 | { 106 | var declaringType = paramType.DeclaringType; 107 | var declaringTypeGenericArgsCount = declaringType.GetGenericArguments().Length; 108 | var declaringTypeName = !declaringType.IsGenericType 109 | ? GetParamTypeName(declaringType) 110 | : GetNestedGenericParamTypeName(declaringType, allGenericArgs.Take(declaringTypeGenericArgsCount)); 111 | 112 | var genericArgsCount = paramType.GetGenericArguments().Length - declaringTypeGenericArgsCount; 113 | var genericArgsNames = genericArgsCount > 0 114 | ? $"{{{string.Join(",", allGenericArgs.TakeLast(genericArgsCount).Select(x => GetParamTypeName(x)))}}}" 115 | : ""; 116 | 117 | return $"{declaringTypeName}.{paramType.Name.Split('`')[0]}{genericArgsNames}"; 118 | } 119 | 120 | private static string GetSimpleParamTypeName(Type paramType) 121 | { 122 | if (paramType.IsNested) return $"{GetParamTypeName(paramType.DeclaringType)}.{paramType.Name}"; 123 | return paramType.FullName ?? $"{paramType.Namespace}.{paramType.Name}"; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Collections.Generic; 3 | 4 | namespace TestWebApi.Controllers 5 | { 6 | [ApiExplorerSettings(GroupName = "controllers")] 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class TestController : ControllerBase 10 | { 11 | [ProducesResponseType(typeof(int), 200)] 12 | [ProducesResponseType(typeof(string), 400)] 13 | [HttpGet] 14 | public IEnumerable Get() 15 | { 16 | return default; 17 | } 18 | 19 | [HttpPost] 20 | public int Post([FromBody] WeatherForecast body) 21 | { 22 | return body.TemperatureC; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/Hubs/StronglyTypedTestHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using SignalRSwaggerGen.Attributes; 4 | using SignalRSwaggerGen.Enums; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace TestWebApi.Hubs 9 | { 10 | [SignalRHub(autoDiscover: AutoDiscover.MethodsAndParams, documentNames: new[] { "hubs" })] 11 | public class StronglyTypedTestHub : Hub 12 | { 13 | public async Task TestMethod() 14 | { 15 | await Clients.All.TestMethod(default, default, default, default); 16 | } 17 | } 18 | 19 | [SignalRHub(autoDiscover: AutoDiscover.MethodsAndParams, documentNames: new[] { "hubs" })] 20 | public interface IStronglyTypedTestHub 21 | { 22 | [Authorize] 23 | [return: SignalRReturn] 24 | [SignalRMethod(summary: "method1 summary", description: "method1 description", autoDiscover: AutoDiscover.Params)] 25 | public ValueTask TestMethod( 26 | int agr1, 27 | string arg2, 28 | [SignalRParam(description: "arg3 description")] WeatherForecast arg3, 29 | [SignalRHidden] CancellationToken cancellationToken); 30 | 31 | [return: SignalRReturn(typeof(Task), 200, "Success")] 32 | [return: SignalRReturn(returnType: typeof(ValueTask<>), statusCode: 201, description: "Created")] 33 | [SignalRMethod(summary: "method2 summary", description: "method2 description", autoDiscover: AutoDiscover.Params)] 34 | public void TestMethod2( 35 | [SignalRParam(description: "arg1 description")] int agr1, 36 | [SignalRParam(description: "arg2 description")] string arg2, 37 | WeatherForecast arg3, 38 | [SignalRHidden] CancellationToken cancellationToken); 39 | 40 | [SignalRRequestBody(typeof(WeatherForecast), false, "request body description")] 41 | [return: SignalRHidden] 42 | public Task TestMethod3( 43 | [SignalRParam(paramType: typeof(WeatherForecast))] int agr1, 44 | string arg2, 45 | WeatherForecast arg3, 46 | [SignalRHidden] CancellationToken cancellationToken); 47 | 48 | [return: SignalRHidden] 49 | public Task TestMethod3(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/Hubs/TestHub.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.SignalR; 3 | using SignalRSwaggerGen.Attributes; 4 | using SignalRSwaggerGen.Enums; 5 | using SignalRSwaggerGen.Naming; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace TestWebApi.Hubs 10 | { 11 | [Authorize] 12 | [SignalRHub(autoDiscover: AutoDiscover.MethodsAndParams, documentNames: new[] { "hubs" }, nameTransformerType: typeof(ToLowerTransformer))] 13 | public class TestHub : Hub 14 | { 15 | [AllowAnonymous] 16 | [return: SignalRReturn] 17 | [SignalRMethod(summary: "method1 summary", description: "method1 description", autoDiscover: AutoDiscover.Params)] 18 | public ValueTask TestMethod( 19 | int agr1, 20 | string arg2, 21 | [SignalRParam(description: "arg3 description")] WeatherForecast arg3, 22 | [SignalRHidden] CancellationToken cancellationToken) 23 | { 24 | return default; 25 | } 26 | 27 | [return: SignalRReturn(typeof(Task), 200, "Success")] 28 | [return: SignalRReturn(returnType: typeof(ValueTask<>), statusCode: 201, description: "Created")] 29 | [SignalRMethod(summary: "method2 summary", description: "method2 description", autoDiscover: AutoDiscover.Params)] 30 | public void TestMethod2( 31 | [SignalRParam(description: "arg1 description")] int agr1, 32 | [SignalRParam(description: "arg2 description")] string arg2, 33 | WeatherForecast arg3, 34 | [SignalRHidden] CancellationToken cancellationToken) 35 | { 36 | return; 37 | } 38 | 39 | [SignalRRequestBody(typeof(WeatherForecast), false, "request body description")] 40 | [return: SignalRHidden] 41 | public Task TestMethod3( 42 | int agr1, 43 | string arg2, 44 | WeatherForecast arg3, 45 | [SignalRHidden] CancellationToken cancellationToken) 46 | { 47 | return default; 48 | } 49 | 50 | [return: SignalRHidden] 51 | public Task TestMethod3() 52 | { 53 | return default; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/Hubs/XmlCommentsHub.cs: -------------------------------------------------------------------------------- 1 | using SignalRSwaggerGen.Attributes; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace TestWebApi.Hubs 6 | { 7 | public class Outer 8 | { 9 | public class Middle 10 | { 11 | public class Inner 12 | { 13 | public class GenericClass { } 14 | 15 | /// 16 | /// xml commented hub 17 | /// 18 | /// 19 | /// 20 | [SignalRHub(documentNames: new[] { "hubs" })] 21 | public class XmlCommentedHub 22 | { 23 | public struct Struct 24 | { 25 | public struct GenericStruct 26 | { 27 | public interface IInterface 28 | { 29 | public delegate int Delegate(); 30 | } 31 | } 32 | } 33 | 34 | /// 35 | /// xml commented method1 summary 36 | /// 37 | /// 38 | /// 39 | /// xml commented arg1 description 40 | /// xml commented arg2 description 41 | /// xml commented arg3 description 42 | /// xml commented arg4 description 43 | /// something 44 | public unsafe void Method1( 45 | ref Middle.Inner.GenericClass, TO1, TO2[,]>[,,,][][,,] arg1, 46 | Struct.GenericStruct.IInterface>[,], GenericClass, Inner>>.Delegate[]>>* arg2, 47 | List, TC1>, ISet[,,][][]>>> arg3, 48 | List.IInterface.Delegate.IInterface.Delegate[,,,,,,,][][]>>, TC1>, ISet[,,][][]>>> arg4) 49 | { 50 | return; 51 | } 52 | 53 | /// 54 | /// xml commented method2 summary 55 | /// 56 | /// 57 | /// 58 | public void Method2() 59 | { 60 | return; 61 | } 62 | 63 | /// 64 | /// xml commented method3 summary 65 | /// 66 | public void Method3() 67 | { 68 | return; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace TestWebApi 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:61479", 8 | "sslPort": 44392 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "TestWebApi": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": "true", 23 | "launchBrowser": true, 24 | "launchUrl": "swagger", 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.OpenApi.Models; 7 | 8 | namespace TestWebApi 9 | { 10 | internal class Startup 11 | { 12 | public Startup(IConfiguration configuration) 13 | { 14 | Configuration = configuration; 15 | } 16 | 17 | public IConfiguration Configuration { get; } 18 | 19 | // This method gets called by the runtime. Use this method to add services to the container. 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddControllers(); 23 | services.AddSwaggerGen(options => 24 | { 25 | var apiInfo = new OpenApiInfo { Title = "TestWebApi", Version = "v1" }; 26 | options.SwaggerDoc("controllers", apiInfo); 27 | options.SwaggerDoc("hubs", apiInfo); 28 | options.IncludeXmlComments("TestWebApi.xml", true); 29 | options.AddSignalRSwaggerGen(); 30 | }); 31 | } 32 | 33 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 34 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 35 | { 36 | if (env.IsDevelopment()) 37 | { 38 | app.UseDeveloperExceptionPage(); 39 | app.UseSwagger(); 40 | app.UseSwaggerUI(options => 41 | { 42 | options.SwaggerEndpoint("/swagger/controllers/swagger.json", "REST API"); 43 | options.SwaggerEndpoint("/swagger/hubs/swagger.json", "SignalR"); 44 | }); 45 | } 46 | 47 | app.UseHttpsRedirection(); 48 | 49 | app.UseRouting(); 50 | 51 | app.UseAuthorization(); 52 | 53 | app.UseEndpoints(endpoints => 54 | { 55 | endpoints.MapControllers(); 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/TestWebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | C:\Projects\SignalRSwaggerGen\SignalRSwaggerGen\TestWebApi\TestWebApi.xml 9 | true 10 | 11 | 12 | 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/TestWebApi.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TestWebApi 5 | 6 | 7 | 8 | 9 | xml commented hub 10 | 11 | 12 | 13 | 14 | 15 | 16 | xml commented method1 summary 17 | 18 | 19 | 20 | xml commented arg1 description 21 | xml commented arg2 description 22 | xml commented arg3 description 23 | xml commented arg4 description 24 | something 25 | 26 | 27 | 28 | xml commented method2 summary 29 | 30 | 31 | 32 | 33 | 34 | 35 | xml commented method3 summary 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TestWebApi 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SignalRSwaggerGen/TestWebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | --------------------------------------------------------------------------------