├── .github └── workflows │ ├── build_nuget.yml │ └── publish_nuget.yml ├── .gitignore ├── .nuke ├── build.schema.json └── parameters.json ├── FS.Keycloak.RestApiClient.png ├── FS.Keycloak.RestApiClient ├── FS.Keycloak.RestApiClient.sln.DotSettings └── src │ ├── FS.Keycloak.RestApiClient.Test │ └── Authentication │ │ └── AuthenticationTests.cs │ └── FS.Keycloak.RestApiClient │ ├── Api │ └── .editorconfig │ ├── Authentication │ ├── Client │ │ ├── AuthenticationHttpClient.cs │ │ ├── ClientCredentialsGrantHttpClient.cs │ │ ├── DirectTokenHttpClient.cs │ │ ├── KeycloakHttpClient.cs │ │ └── PasswordGrantHttpClient.cs │ ├── ClientFactory │ │ └── AuthenticationHttpClientFactory.cs │ ├── Flow │ │ ├── AuthenticationFlow.cs │ │ ├── ClientCredentialsFlow.cs │ │ ├── DirectTokenFlow.cs │ │ └── PasswordGrantFlow.cs │ └── Model │ │ └── KeycloakApiToken.cs │ ├── Client │ └── .editorconfig │ ├── ClientFactory │ └── ApiClientFactory.cs │ └── ContractResolver │ └── SnakeCaseContractResolver.cs ├── README.md ├── build.cmd ├── build.ps1 ├── build.sh └── build ├── .editorconfig ├── Build.cs ├── Configuration.cs ├── Directory.Build.props ├── Directory.Build.targets ├── Properties └── launchSettings.json ├── _build.csproj ├── _build.csproj.DotSettings ├── _build.sln ├── openapi-generator ├── openapi-generator.config.json ├── openapitools.json ├── package-lock.json ├── package.json └── templates │ ├── api.mustache │ └── modelGeneric.mustache ├── services ├── BuildPaths.cs ├── ClientGenerator.cs ├── Npm.cs ├── OpenApiSpec.cs └── Project.cs └── targets ├── net_standard.props ├── nuget.props └── version.props /.github/workflows/build_nuget.yml: -------------------------------------------------------------------------------- 1 | name: Build NuGet Packages 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build_nuget: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Setup .NET 12 | uses: actions/setup-dotnet@v4 13 | with: 14 | dotnet-version: 8.0.x 15 | - name: Setup Node 20.x 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '20.x' 19 | - name: Setup Java 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: 'microsoft' 23 | java-version: '21' 24 | - name: Build NuGet package 25 | env: 26 | OpenApiJson: https://www.keycloak.org/docs-api/26.2.0/rest-api/openapi.json 27 | PublishFolder: Keycloak.RestApiClient.${{github.ref_name}} 28 | NuGetApiKey: ${{ secrets.NUGET_API_KEY }} 29 | run: ./build.cmd BuildClient 30 | -------------------------------------------------------------------------------- /.github/workflows/publish_nuget.yml: -------------------------------------------------------------------------------- 1 | name: Publish NuGet Packages 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | publish_nuget: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup .NET 14 | uses: actions/setup-dotnet@v4 15 | with: 16 | dotnet-version: 8.0.x 17 | - name: Setup Node 20.x 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '20.x' 21 | - name: Setup Java 22 | uses: actions/setup-java@v4 23 | with: 24 | distribution: 'microsoft' 25 | java-version: '21' 26 | - name: Build NuGet package 27 | env: 28 | OpenApiJson: https://www.keycloak.org/docs-api/26.2.0/rest-api/openapi.json 29 | PublishFolder: Keycloak.RestApiClient.${{github.ref_name}} 30 | NuGetApiKey: ${{ secrets.NUGET_API_KEY }} 31 | run: ./build.cmd PublishClient 32 | - name: Upload NuGet package 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: Keycloak.RestApiClient.${{github.ref_name}} 36 | path: Keycloak.RestApiClient.${{github.ref_name}} 37 | -------------------------------------------------------------------------------- /.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 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # Benchmark Results 46 | BenchmarkDotNet.Artifacts/ 47 | 48 | # .NET Core 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | **/Properties/launchSettings.json 53 | 54 | *_i.c 55 | *_p.c 56 | *_i.h 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.svclog 77 | *.scc 78 | 79 | # Chutzpah Test files 80 | _Chutzpah* 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opendb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | *.VC.db 91 | *.VC.VC.opendb 92 | 93 | # Visual Studio profiler 94 | *.psess 95 | *.vsp 96 | *.vspx 97 | *.sap 98 | 99 | # TFS 2012 Local Workspace 100 | $tf/ 101 | 102 | # Guidance Automation Toolkit 103 | *.gpState 104 | 105 | # ReSharper is a .NET coding add-in 106 | _ReSharper*/ 107 | *.[Rr]e[Ss]harper 108 | *.DotSettings.user 109 | 110 | # JustCode is a .NET coding add-in 111 | .JustCode 112 | 113 | # TeamCity is a build add-in 114 | _TeamCity* 115 | 116 | # DotCover is a Code Coverage Tool 117 | *.dotCover 118 | 119 | # Visual Studio code coverage results 120 | *.coverage 121 | *.coveragexml 122 | 123 | # NCrunch 124 | _NCrunch_* 125 | .*crunch*.local.xml 126 | nCrunchTemp_* 127 | 128 | # MightyMoose 129 | *.mm.* 130 | AutoTest.Net/ 131 | 132 | # Web workbench (sass) 133 | .sass-cache/ 134 | 135 | # Installshield output folder 136 | [Ee]xpress/ 137 | 138 | # DocProject is a documentation generator add-in 139 | DocProject/buildhelp/ 140 | DocProject/Help/*.HxT 141 | DocProject/Help/*.HxC 142 | DocProject/Help/*.hhc 143 | DocProject/Help/*.hhk 144 | DocProject/Help/*.hhp 145 | DocProject/Help/Html2 146 | DocProject/Help/html 147 | 148 | # Click-Once directory 149 | publish/ 150 | 151 | # Publish Web Output 152 | *.[Pp]ublish.xml 153 | *.azurePubxml 154 | # TODO: Comment the next line if you want to checkin your web deploy settings 155 | # but database connection strings (with potential passwords) will be unencrypted 156 | *.pubxml 157 | *.publishproj 158 | 159 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 160 | # checkin your Azure Web App publish settings, but sensitive information contained 161 | # in these scripts will be unencrypted 162 | PublishScripts/ 163 | 164 | # NuGet Packages 165 | *.nupkg 166 | # The packages folder can be ignored because of Package Restore 167 | **/packages/* 168 | # except build/, which is used as an MSBuild target. 169 | !**/packages/build/ 170 | # Uncomment if necessary however generally it will be regenerated when needed 171 | #!**/packages/repositories.config 172 | # NuGet v3's project.json files produces more ignorable files 173 | *.nuget.props 174 | *.nuget.targets 175 | 176 | # Microsoft Azure Build Output 177 | csx/ 178 | *.build.csdef 179 | 180 | # Microsoft Azure Emulator 181 | ecf/ 182 | rcf/ 183 | 184 | # Windows Store app package directories and files 185 | AppPackages/ 186 | BundleArtifacts/ 187 | Package.StoreAssociation.xml 188 | _pkginfo.txt 189 | *.appx 190 | 191 | # Visual Studio cache files 192 | # files ending in .cache can be ignored 193 | *.[Cc]ache 194 | # but keep track of directories ending in .cache 195 | !*.[Cc]ache/ 196 | 197 | # Others 198 | ClientBin/ 199 | ~$* 200 | *~ 201 | *.dbmdl 202 | *.dbproj.schemaview 203 | *.jfm 204 | *.pfx 205 | *.publishsettings 206 | orleans.codegen.cs 207 | 208 | # Since there are multiple workflows, uncomment next line to ignore bower_components 209 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 210 | #bower_components/ 211 | 212 | # RIA/Silverlight projects 213 | Generated_Code/ 214 | 215 | # Backup & report files from converting an old project file 216 | # to a newer Visual Studio version. Backup files are not needed, 217 | # because we have git ;-) 218 | _UpgradeReport_Files/ 219 | Backup*/ 220 | UpgradeLog*.XML 221 | UpgradeLog*.htm 222 | 223 | # SQL Server files 224 | *.mdf 225 | *.ldf 226 | *.ndf 227 | 228 | # Business Intelligence projects 229 | *.rdl.data 230 | *.bim.layout 231 | *.bim_*.settings 232 | 233 | # Microsoft Fakes 234 | FakesAssemblies/ 235 | 236 | # GhostDoc plugin setting file 237 | *.GhostDoc.xml 238 | 239 | # Node.js Tools for Visual Studio 240 | .ntvs_analysis.dat 241 | node_modules/ 242 | 243 | # Typescript v1 declaration files 244 | typings/ 245 | 246 | # Visual Studio 6 build log 247 | *.plg 248 | 249 | # Visual Studio 6 workspace options file 250 | *.opt 251 | 252 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 253 | *.vbw 254 | 255 | # Visual Studio LightSwitch build output 256 | **/*.HTMLClient/GeneratedArtifacts 257 | **/*.DesktopClient/GeneratedArtifacts 258 | **/*.DesktopClient/ModelManifest.xml 259 | **/*.Server/GeneratedArtifacts 260 | **/*.Server/ModelManifest.xml 261 | _Pvt_Extensions 262 | 263 | # Paket dependency manager 264 | .paket/paket.exe 265 | paket-files/ 266 | 267 | # FAKE - F# Make 268 | .fake/ 269 | 270 | # JetBrains Rider 271 | .idea/ 272 | *.sln.iml 273 | 274 | # CodeRush 275 | .cr/ 276 | 277 | # Python Tools for Visual Studio (PTVS) 278 | __pycache__/ 279 | *.pyc 280 | 281 | # Cake - Uncomment if you are using it 282 | # tools/** 283 | # !tools/packages.config 284 | 285 | # Tabs Studio 286 | *.tss 287 | 288 | # Telerik's JustMock configuration file 289 | *.jmconfig 290 | 291 | # BizTalk build output 292 | *.btp.cs 293 | *.btm.cs 294 | *.odx.cs 295 | *.xsd.cs 296 | 297 | # User added stuff 298 | .openapi-generator/ 299 | /keycloak.openapi.json 300 | /keycloak.openapi.*.json 301 | /FS.Keycloak.RestApiClient/.gitignore 302 | /FS.Keycloak.RestApiClient/.openapi-generator-ignore 303 | /FS.Keycloak.RestApiClient/FS.Keycloak.RestApiClient.sln 304 | /FS.Keycloak.RestApiClient/README.md 305 | /FS.Keycloak.RestApiClient/appveyor.yml 306 | /FS.Keycloak.RestApiClient/docs 307 | /FS.Keycloak.RestApiClient/git_push.sh 308 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Api 309 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Client 310 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Model 311 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/FS.Keycloak.RestApiClient.csproj 312 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient.Test/Api 313 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient.Test/Model 314 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient.Test/FS.Keycloak.RestApiClient.Test.csproj 315 | /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient.Test/Authentication/AuthenticationTests.data.json 316 | /FS.Keycloak.RestApiClient/api/openapi.yaml 317 | -------------------------------------------------------------------------------- /.nuke/build.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "definitions": { 4 | "Host": { 5 | "type": "string", 6 | "enum": [ 7 | "AppVeyor", 8 | "AzurePipelines", 9 | "Bamboo", 10 | "Bitbucket", 11 | "Bitrise", 12 | "GitHubActions", 13 | "GitLab", 14 | "Jenkins", 15 | "Rider", 16 | "SpaceAutomation", 17 | "TeamCity", 18 | "Terminal", 19 | "TravisCI", 20 | "VisualStudio", 21 | "VSCode" 22 | ] 23 | }, 24 | "ExecutableTarget": { 25 | "type": "string", 26 | "enum": [ 27 | "BuildClient", 28 | "GenerateClient", 29 | "ProvideOpenApiSpec", 30 | "PublishClient", 31 | "Restore" 32 | ] 33 | }, 34 | "Verbosity": { 35 | "type": "string", 36 | "description": "", 37 | "enum": [ 38 | "Verbose", 39 | "Normal", 40 | "Minimal", 41 | "Quiet" 42 | ] 43 | }, 44 | "NukeBuild": { 45 | "properties": { 46 | "Continue": { 47 | "type": "boolean", 48 | "description": "Indicates to continue a previously failed build attempt" 49 | }, 50 | "Help": { 51 | "type": "boolean", 52 | "description": "Shows the help text for this build assembly" 53 | }, 54 | "Host": { 55 | "description": "Host for execution. Default is 'automatic'", 56 | "$ref": "#/definitions/Host" 57 | }, 58 | "NoLogo": { 59 | "type": "boolean", 60 | "description": "Disables displaying the NUKE logo" 61 | }, 62 | "Partition": { 63 | "type": "string", 64 | "description": "Partition to use on CI" 65 | }, 66 | "Plan": { 67 | "type": "boolean", 68 | "description": "Shows the execution plan (HTML)" 69 | }, 70 | "Profile": { 71 | "type": "array", 72 | "description": "Defines the profiles to load", 73 | "items": { 74 | "type": "string" 75 | } 76 | }, 77 | "Root": { 78 | "type": "string", 79 | "description": "Root directory during build execution" 80 | }, 81 | "Skip": { 82 | "type": "array", 83 | "description": "List of targets to be skipped. Empty list skips all dependencies", 84 | "items": { 85 | "$ref": "#/definitions/ExecutableTarget" 86 | } 87 | }, 88 | "Target": { 89 | "type": "array", 90 | "description": "List of targets to be invoked. Default is '{default_target}'", 91 | "items": { 92 | "$ref": "#/definitions/ExecutableTarget" 93 | } 94 | }, 95 | "Verbosity": { 96 | "description": "Logging verbosity during build execution. Default is 'Normal'", 97 | "$ref": "#/definitions/Verbosity" 98 | } 99 | } 100 | } 101 | }, 102 | "allOf": [ 103 | { 104 | "properties": { 105 | "NuGetApiKey": { 106 | "type": "string", 107 | "default": "Secrets must be entered via 'nuke :secrets [profile]'" 108 | }, 109 | "OpenApiJson": { 110 | "type": "string", 111 | "description": "Path or download URL for Open API definition to build client for" 112 | }, 113 | "PublishFolder": { 114 | "type": "string" 115 | }, 116 | "Version": { 117 | "type": "string" 118 | } 119 | } 120 | }, 121 | { 122 | "$ref": "#/definitions/NukeBuild" 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /.nuke/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "build.schema.json" 3 | } 4 | -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fschick/Keycloak.RestApiClient/07cd2705d15ef8a7bc9c58674c45dd4c9f77c659/FS.Keycloak.RestApiClient.png -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/FS.Keycloak.RestApiClient.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient.Test/Authentication/AuthenticationTests.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.ClientFactory; 2 | using FS.Keycloak.RestApiClient.Authentication.Flow; 3 | using Newtonsoft.Json; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Xunit; 12 | 13 | namespace FS.Keycloak.RestApiClient.Test.Authentication 14 | { 15 | public class AuthenticationTests 16 | { 17 | [Theory] 18 | [ClassData(typeof(AuthenticationTestDataGenerator))] 19 | public async Task WhenClientAuthenticationIsProvided_UserInfoCanBeRequested(AuthenticationTestData authenticationTestData) 20 | { 21 | if (authenticationTestData == null) 22 | return; 23 | 24 | if (authenticationTestData.ClientCredentialsFlow != null) 25 | await TestClientCredentialsFlow(authenticationTestData); 26 | 27 | if (authenticationTestData.PasswordGrantFlow != null) 28 | await TestPasswordGrantFlow(authenticationTestData); 29 | } 30 | 31 | private static async Task TestClientCredentialsFlow(AuthenticationTestData authenticationTestData) 32 | { 33 | var url = authenticationTestData.KeycloakUrl; 34 | var realm = authenticationTestData.KeycloakRealm; 35 | 36 | var clientCredentials = new ClientCredentialsFlow 37 | { 38 | KeycloakUrl = url, 39 | Realm = realm, 40 | ClientId = authenticationTestData.ClientCredentialsFlow.ClientId, 41 | ClientSecret = authenticationTestData.ClientCredentialsFlow.ClientSecret, 42 | Scope = "openid", 43 | }; 44 | 45 | using var httpClient = AuthenticationHttpClientFactory.Create(clientCredentials); 46 | using var request = new HttpRequestMessage(HttpMethod.Get, $"{url}/realms/{realm}/protocol/openid-connect/userinfo"); 47 | using var response = await httpClient.SendAsync(request, CancellationToken.None); 48 | Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); 49 | } 50 | 51 | private static async Task TestPasswordGrantFlow(AuthenticationTestData authenticationTestData) 52 | { 53 | var url = authenticationTestData.KeycloakUrl; 54 | var realm = authenticationTestData.KeycloakRealm; 55 | 56 | var clientCredentials = new PasswordGrantFlow 57 | { 58 | KeycloakUrl = url, 59 | Realm = realm, 60 | UserName = authenticationTestData.PasswordGrantFlow.Username, 61 | Password = authenticationTestData.PasswordGrantFlow.Password, 62 | Scope = "openid", 63 | }; 64 | 65 | using var httpClient = AuthenticationHttpClientFactory.Create(clientCredentials); 66 | using var request = new HttpRequestMessage(HttpMethod.Get, $"{url}/realms/{realm}/protocol/openid-connect/userinfo"); 67 | using var response = await httpClient.SendAsync(request, CancellationToken.None); 68 | Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); 69 | } 70 | } 71 | 72 | public class AuthenticationTestData 73 | { 74 | public string KeycloakUrl { get; set; } 75 | 76 | public string KeycloakRealm { get; set; } 77 | 78 | public ClientCredentialsTestData ClientCredentialsFlow { get; set; } 79 | 80 | public PasswordGrantTestData PasswordGrantFlow { get; set; } 81 | 82 | public class ClientCredentialsTestData 83 | { 84 | public string ClientId { get; set; } 85 | public string ClientSecret { get; set; } 86 | } 87 | 88 | public class PasswordGrantTestData 89 | { 90 | public string Username { get; set; } 91 | public string Password { get; set; } 92 | } 93 | } 94 | 95 | public class AuthenticationTestDataGenerator : IEnumerable 96 | { 97 | private readonly IEnumerable _data = GetTestData(); 98 | 99 | public IEnumerator GetEnumerator() => _data.GetEnumerator(); 100 | 101 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 102 | 103 | private static IEnumerable GetTestData() 104 | { 105 | const string testDataFilename = "Authentication/AuthenticationTests.data.json"; 106 | if (!File.Exists(testDataFilename)) 107 | return new List { new object[] { null } }; 108 | 109 | var testDataJson = File.ReadAllText(testDataFilename); 110 | var testDataList = JsonConvert.DeserializeObject>(testDataJson); 111 | return testDataList.Select(testData => new[] { testData }).ToList(); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Api/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | generated_code = true 3 | dotnet_analyzer_diagnostic.severity = none -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Client/AuthenticationHttpClient.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Flow; 2 | using FS.Keycloak.RestApiClient.Client; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Net.Http; 7 | 8 | namespace FS.Keycloak.RestApiClient.Authentication.Client 9 | { 10 | /// 11 | /// Base class for authentication clients. 12 | /// 13 | [SuppressMessage("ReSharper", "MemberCanBeProtected.Global")] 14 | public abstract class AuthenticationHttpClient : HttpClient 15 | { 16 | /// 17 | /// JSON serializer settings for Keycloak API. 18 | /// 19 | protected static readonly JsonSerializerSettings KeycloakJsonSerializerSettings = new JsonSerializerSettings { ContractResolver = new SnakeCaseContractResolver() }; 20 | 21 | /// 22 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/ 23 | /// 24 | public string KeycloakUrl { get; } 25 | 26 | /// 27 | /// URL to keycloak server's token endpoint 28 | /// 29 | public string AuthTokenUrl { get; } 30 | 31 | /// 32 | /// Creates a HttpClient authenticated against a Keycloak server. 33 | /// 34 | /// Data used for authentication. 35 | protected AuthenticationHttpClient(AuthenticationFlow flow) 36 | : this(flow, new HttpClientHandler(), true) { } 37 | 38 | /// 39 | /// Creates a HttpClient authenticated against a Keycloak server. 40 | /// 41 | /// Data used for authentication. 42 | /// The responsible for processing the HTTP response messages. 43 | /// if the inner handler should be disposed of by HttpClient.Dispose; if you intend to reuse the inner handler. 44 | protected AuthenticationHttpClient(AuthenticationFlow flow, HttpMessageHandler handler, bool disposeHandler) 45 | : base(handler ?? throw new ArgumentNullException(nameof(handler)), disposeHandler) 46 | { 47 | if (flow == null) 48 | throw new ArgumentNullException(nameof(flow)); 49 | 50 | KeycloakUrl = flow.KeycloakUrl; 51 | AuthTokenUrl = $"{KeycloakUrl}/realms/{flow.Realm}/protocol/openid-connect/token"; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Client/ClientCredentialsGrantHttpClient.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Client; 2 | using FS.Keycloak.RestApiClient.Authentication.Flow; 3 | using FS.Keycloak.RestApiClient.Model; 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace FS.Keycloak.RestApiClient.Authentication.Client 14 | { 15 | /// 16 | public class ClientCredentialsGrantHttpClient : AuthenticationHttpClient 17 | { 18 | private KeycloakApiToken _token; 19 | private readonly Dictionary _parameters; 20 | 21 | 22 | /// 23 | public ClientCredentialsGrantHttpClient(AuthenticationFlow flow) 24 | : base(flow) { } 25 | 26 | /// 27 | public ClientCredentialsGrantHttpClient(ClientCredentialsFlow flow, HttpMessageHandler handler, bool disposeHandler) 28 | : base(flow, handler, disposeHandler) 29 | { 30 | _parameters = new Dictionary 31 | { 32 | { "grant_type", "client_credentials" }, 33 | { "client_id", flow.ClientId }, 34 | { "client_secret", flow.ClientSecret }, 35 | }; 36 | 37 | if (!string.IsNullOrWhiteSpace(flow.Scope)) 38 | _parameters.Add("scope", flow.Scope); 39 | } 40 | 41 | /// 42 | public override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 43 | { 44 | await AddAuthorizationHeader(request, cancellationToken); 45 | return await base.SendAsync(request, cancellationToken); 46 | } 47 | 48 | private async Task AddAuthorizationHeader(HttpRequestMessage request, CancellationToken cancellationToken) 49 | { 50 | if (_token == null || _token.IsExpired) 51 | _token = await GetToken(cancellationToken); 52 | 53 | request.Headers.Authorization = new AuthenticationHeaderValue("bearer", _token.AccessToken); 54 | } 55 | 56 | private async Task GetToken(CancellationToken cancellationToken) 57 | { 58 | using (var tokenRequest = new HttpRequestMessage(HttpMethod.Post, AuthTokenUrl)) 59 | { 60 | tokenRequest.Content = new FormUrlEncodedContent(_parameters); 61 | using (var response = await base.SendAsync(tokenRequest, cancellationToken)) 62 | { 63 | if (response.StatusCode != HttpStatusCode.OK) 64 | throw new Exception($"Client credentials authentication failed with code: {response.StatusCode}"); 65 | 66 | var tokenJson = await response.Content.ReadAsStringAsync(); 67 | var token = JsonConvert.DeserializeObject(tokenJson, KeycloakJsonSerializerSettings); 68 | return token; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | namespace FS.Keycloak.RestApiClient.Client.Auth 76 | { 77 | /// 78 | [Obsolete("Use ClientCredentialsGrantHttpClient instead.")] 79 | public class AuthClientClientCredentials : ClientCredentialsGrantHttpClient 80 | { 81 | /// 82 | public AuthClientClientCredentials(ClientCredentials flow, HttpMessageHandler handler = null, bool disposeHandler = true) 83 | : base(flow, handler, disposeHandler) { } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Client/DirectTokenHttpClient.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Client; 2 | using FS.Keycloak.RestApiClient.Authentication.Flow; 3 | using FS.Keycloak.RestApiClient.Model; 4 | using System; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace FS.Keycloak.RestApiClient.Authentication.Client 11 | { 12 | /// 13 | public class DirectTokenHttpClient : AuthenticationHttpClient 14 | { 15 | private readonly string _token; 16 | 17 | /// 18 | public DirectTokenHttpClient(AuthenticationFlow flow) 19 | : base(flow) { } 20 | 21 | /// 22 | public DirectTokenHttpClient(DirectTokenFlow flow, HttpMessageHandler handler, bool disposeHandler) 23 | : base(flow, handler, disposeHandler) 24 | => _token = flow.Token; 25 | 26 | /// 27 | public override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 28 | { 29 | AddAuthorizationHeader(request); 30 | return await base.SendAsync(request, cancellationToken); 31 | } 32 | 33 | private void AddAuthorizationHeader(HttpRequestMessage request) 34 | => request.Headers.Authorization = new AuthenticationHeaderValue("bearer", _token); 35 | } 36 | } 37 | 38 | namespace FS.Keycloak.RestApiClient.Client.Auth 39 | { 40 | /// 41 | [Obsolete("Use DirectTokenHttpClient instead.")] 42 | public class AuthClientDirectToken : DirectTokenHttpClient 43 | { 44 | /// 45 | public AuthClientDirectToken(DirectToken flow, HttpMessageHandler handler = null, bool disposeHandler = true) 46 | : base(flow, handler, disposeHandler) { } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Client/KeycloakHttpClient.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Client; 2 | using FS.Keycloak.RestApiClient.Authentication.Flow; 3 | using System; 4 | using System.Net.Http; 5 | 6 | // ReSharper disable once CheckNamespace 7 | namespace FS.Keycloak.RestApiClient.Client 8 | { 9 | /// 10 | [Obsolete("Use AuthenticationHttpClientFactory instead")] 11 | public sealed class KeycloakHttpClient : PasswordGrantHttpClient 12 | { 13 | /// 14 | /// Creates a HttpClient authenticated against a Keycloak server. 15 | /// 16 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/ 17 | /// Username to authenticate with. 18 | /// Password for the user to authenticate. 19 | [Obsolete("Use AuthenticationHttpClientFactory with explicit authentication flow parameter.")] 20 | public KeycloakHttpClient(string authServerUrl, string user, string password) 21 | : this(authServerUrl, user, password, new HttpClientHandler()) { } 22 | 23 | /// 24 | /// Creates a HttpClient authenticated against a Keycloak server. 25 | /// 26 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/ 27 | /// The realm to authenticate against. 28 | /// Username to authenticate with. 29 | /// Password for the user to authenticate. 30 | [Obsolete("Use AuthenticationHttpClientFactory with explicit authentication flow parameter.")] 31 | public KeycloakHttpClient(string authServerUrl, string realm, string user, string password) 32 | : this(authServerUrl, realm, user, password, new HttpClientHandler()) { } 33 | 34 | /// 35 | /// Creates a HttpClient authenticated against a Keycloak server. 36 | /// 37 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/ 38 | /// Username to authenticate with. 39 | /// Password for the user to authenticate. 40 | /// The responsible for processing the HTTP response messages. 41 | [Obsolete("Use AuthenticationHttpClientFactory with explicit authentication flow parameter.")] 42 | public KeycloakHttpClient(string authServerUrl, string user, string password, HttpMessageHandler handler) 43 | : this(authServerUrl, user, password, handler, true) { } 44 | 45 | /// 46 | /// Creates a HttpClient authenticated against a Keycloak server. 47 | /// 48 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/ 49 | /// The realm to authenticate against. 50 | /// Username to authenticate with. 51 | /// Password for the user to authenticate. 52 | /// The responsible for processing the HTTP response messages. 53 | [Obsolete("Use AuthenticationHttpClientFactory with explicit authentication flow parameter.")] 54 | public KeycloakHttpClient(string authServerUrl, string realm, string user, string password, HttpMessageHandler handler) 55 | : this(authServerUrl, realm, user, password, handler, true) { } 56 | 57 | /// 58 | /// Creates a HttpClient authenticated against a Keycloak server. 59 | /// 60 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/ 61 | /// Username to authenticate with. 62 | /// Password for the user to authenticate. 63 | /// The responsible for processing the HTTP response messages. 64 | /// if the inner handler should be disposed of by HttpClient.Dispose; if you intend to reuse the inner handler. 65 | [Obsolete("Use AuthenticationHttpClientFactory with explicit authentication flow parameter.")] 66 | public KeycloakHttpClient(string authServerUrl, string user, string password, HttpMessageHandler handler, bool disposeHandler) 67 | : this(authServerUrl, "master", user, password, handler, disposeHandler) { } 68 | 69 | /// 70 | /// Creates a HttpClient authenticated against a Keycloak server. 71 | /// 72 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/ 73 | /// The realm to authenticate against. 74 | /// Username to authenticate with. 75 | /// Password for the user to authenticate. 76 | /// The responsible for processing the HTTP response messages. 77 | /// if the inner handler should be disposed of by HttpClient.Dispose; if you intend to reuse the inner handler. 78 | [Obsolete("Use AuthenticationHttpClientFactory with explicit authentication flow parameter.")] 79 | public KeycloakHttpClient(string authServerUrl, string realm, string user, string password, HttpMessageHandler handler, bool disposeHandler) 80 | : base(GetPasswordGrantFlow(authServerUrl, realm, user, password), handler, disposeHandler) 81 | { 82 | } 83 | 84 | private static PasswordGrantFlow GetPasswordGrantFlow(string authServerUrl, string realm, string user, string password) 85 | => new PasswordGrantFlow 86 | { 87 | KeycloakUrl = authServerUrl, 88 | Realm = realm, 89 | UserName = user, 90 | Password = password 91 | }; 92 | } 93 | } -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Client/PasswordGrantHttpClient.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Client; 2 | using FS.Keycloak.RestApiClient.Authentication.Flow; 3 | using FS.Keycloak.RestApiClient.Model; 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace FS.Keycloak.RestApiClient.Authentication.Client 14 | { 15 | /// 16 | public class PasswordGrantHttpClient : AuthenticationHttpClient 17 | { 18 | private KeycloakApiToken _token; 19 | private readonly Dictionary _parameters; 20 | 21 | /// 22 | public PasswordGrantHttpClient(AuthenticationFlow flow) 23 | : base(flow) { } 24 | 25 | /// 26 | public PasswordGrantHttpClient(PasswordGrantFlow flow, HttpMessageHandler handler, bool disposeHandler) 27 | : base(flow, handler, disposeHandler) 28 | { 29 | _parameters = new Dictionary 30 | { 31 | { "client_id", "admin-cli" }, 32 | { "grant_type", "password" }, 33 | { "username", flow.UserName }, 34 | { "password", flow.Password }, 35 | }; 36 | 37 | if (!string.IsNullOrWhiteSpace(flow.Scope)) 38 | _parameters.Add("scope", flow.Scope); 39 | } 40 | 41 | /// 42 | public override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 43 | { 44 | await AddAuthorizationHeader(request, cancellationToken); 45 | return await base.SendAsync(request, cancellationToken); 46 | } 47 | 48 | private async Task AddAuthorizationHeader(HttpRequestMessage request, CancellationToken cancellationToken) 49 | { 50 | if (_token == null || _token.IsExpired) 51 | _token = await GetToken(cancellationToken); 52 | 53 | request.Headers.Authorization = new AuthenticationHeaderValue("bearer", _token.AccessToken); 54 | } 55 | 56 | private async Task GetToken(CancellationToken cancellationToken) 57 | { 58 | using (var tokenRequest = new HttpRequestMessage(HttpMethod.Post, AuthTokenUrl)) 59 | { 60 | tokenRequest.Content = new FormUrlEncodedContent(_parameters); 61 | using (var response = await base.SendAsync(tokenRequest, cancellationToken)) 62 | { 63 | if (response.StatusCode != HttpStatusCode.OK) 64 | throw new Exception($"Username and password authentication failed with code: {response.StatusCode}"); 65 | 66 | var tokenJson = await response.Content.ReadAsStringAsync(); 67 | var token = JsonConvert.DeserializeObject(tokenJson, KeycloakJsonSerializerSettings); 68 | return token; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | namespace FS.Keycloak.RestApiClient.Client.Auth 76 | { 77 | /// 78 | [Obsolete("Use PasswordGrantHttpClient instead.")] 79 | public class AuthClientPasswordGrant : PasswordGrantHttpClient 80 | { 81 | /// 82 | public AuthClientPasswordGrant(PasswordGrant flow, HttpMessageHandler handler = null, bool disposeHandler = true) 83 | : base(flow, handler, disposeHandler) { } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/ClientFactory/AuthenticationHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Client; 2 | using FS.Keycloak.RestApiClient.Authentication.ClientFactory; 3 | using FS.Keycloak.RestApiClient.Authentication.Flow; 4 | using System; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Net.Http; 7 | 8 | namespace FS.Keycloak.RestApiClient.Authentication.ClientFactory 9 | { 10 | /// 11 | /// Factory to create an authenticated HttpClient used to communicate with Keycloak server. 12 | /// 13 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] 14 | public class AuthenticationHttpClientFactory 15 | { 16 | /// 17 | /// Creates a HttpClient authenticated against a Keycloak server. 18 | /// 19 | /// The type of the authentication flow. 20 | /// Data used for authentication. 21 | public static AuthenticationHttpClient Create(TAuthenticationFlow authenticationFlow) 22 | where TAuthenticationFlow : AuthenticationFlow 23 | => Create(authenticationFlow, new HttpClientHandler(), true); 24 | 25 | /// 26 | /// Creates a HttpClient authenticated against a Keycloak server. 27 | /// 28 | /// The type of the authentication flow. 29 | /// Data used for authentication. 30 | /// The responsible for processing the HTTP response messages. 31 | /// if the inner handler should be disposed of by HttpClient.Dispose; if you intend to reuse the inner handler. 32 | public static AuthenticationHttpClient Create(TAuthenticationFlow authenticationFlow, HttpMessageHandler handler, bool disposeHandler) 33 | where TAuthenticationFlow : AuthenticationFlow 34 | { 35 | switch (authenticationFlow) 36 | { 37 | case ClientCredentialsFlow clientCredentials: 38 | return new ClientCredentialsGrantHttpClient(clientCredentials, handler, disposeHandler); 39 | case PasswordGrantFlow passwordGrant: 40 | return new PasswordGrantHttpClient(passwordGrant, handler, disposeHandler); 41 | case DirectTokenFlow directToken: 42 | return new DirectTokenHttpClient(directToken, handler, disposeHandler); 43 | default: 44 | throw new ArgumentException("Unknown authentication flow parameters"); 45 | } 46 | } 47 | } 48 | } 49 | 50 | namespace FS.Keycloak.RestApiClient.Client.Auth 51 | { 52 | /// 53 | [Obsolete("Use AuthenticationHttpClientFactory instead")] 54 | public class AuthClientFactory : AuthenticationHttpClientFactory 55 | { 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Flow/AuthenticationFlow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FS.Keycloak.RestApiClient.Authentication.Flow 4 | { 5 | /// 6 | /// Base class for authentication flows. 7 | /// 8 | public abstract class AuthenticationFlow 9 | { 10 | private string _realm; 11 | private string _scope; 12 | 13 | /// 14 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/. 15 | /// 16 | public string KeycloakUrl { get; set; } 17 | 18 | /// 19 | /// The realm to authenticate against. 20 | /// 21 | public string Realm { get => _realm ?? "master"; set => _realm = value; } 22 | 23 | /// 24 | /// Base URL to keycloak server, e.g. https://keycloak.example.com:8443/. 25 | /// 26 | [Obsolete("Use KeycloakUrl instead.")] 27 | public string AuthUrl 28 | { 29 | get => KeycloakUrl; 30 | set => KeycloakUrl = value; 31 | } 32 | 33 | /// 34 | /// Space-delimited list of scopes requested by login. 35 | /// 36 | public string Scope 37 | { 38 | get => _scope ?? string.Empty; 39 | set => _scope = value; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Flow/ClientCredentialsFlow.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Flow; 2 | using System; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace FS.Keycloak.RestApiClient.Authentication.Flow 6 | { 7 | /// 8 | /// Authenticate against Keycloak using client credentials flow. 9 | /// 10 | public class ClientCredentialsFlow : AuthenticationFlow 11 | { 12 | /// 13 | /// The client id of the client to authenticate. 14 | /// 15 | public string ClientId { get; set; } 16 | 17 | /// 18 | /// The client secret of the client to authenticate. 19 | /// 20 | public string ClientSecret { get; set; } 21 | } 22 | } 23 | 24 | namespace FS.Keycloak.RestApiClient.Model 25 | { 26 | /// 27 | /// Authenticate against Keycloak using client credentials flow. 28 | /// 29 | [Obsolete("Use ClientCredentialsFlow instead.")] 30 | [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] 31 | public class ClientCredentials : ClientCredentialsFlow 32 | { 33 | } 34 | } -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Flow/DirectTokenFlow.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Flow; 2 | using System; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace FS.Keycloak.RestApiClient.Authentication.Flow 6 | { 7 | /// 8 | /// Authenticate against Keycloak using a direct token. 9 | /// 10 | public class DirectTokenFlow : AuthenticationFlow 11 | { 12 | /// 13 | /// The token to use for authentication. 14 | /// 15 | public string Token { get; set; } 16 | } 17 | } 18 | 19 | namespace FS.Keycloak.RestApiClient.Model 20 | { 21 | /// 22 | /// Authenticate against Keycloak using a direct token. 23 | /// 24 | [Obsolete("Use DirectTokenFlow instead.")] 25 | [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] 26 | public class DirectToken : DirectTokenFlow 27 | { 28 | } 29 | } -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Flow/PasswordGrantFlow.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Flow; 2 | using System; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace FS.Keycloak.RestApiClient.Authentication.Flow 6 | { 7 | /// 8 | /// Authenticate against Keycloak using password grant flow. 9 | /// 10 | public class PasswordGrantFlow : AuthenticationFlow 11 | { 12 | /// 13 | /// Username to authenticate with. 14 | /// 15 | public string UserName { get; set; } 16 | 17 | /// 18 | /// Password for the user to authenticate. 19 | /// 20 | public string Password { get; set; } 21 | } 22 | } 23 | 24 | namespace FS.Keycloak.RestApiClient.Model 25 | { 26 | /// 27 | /// Authenticate against Keycloak using password grant flow. 28 | /// 29 | [Obsolete("Use PasswordGrantFlow instead.")] 30 | [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] 31 | public class PasswordGrant : PasswordGrantFlow 32 | { 33 | } 34 | } -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Authentication/Model/KeycloakApiToken.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace FS.Keycloak.RestApiClient.Model 6 | { 7 | /// 8 | /// Represents a token received from the Keycloak API. 9 | /// 10 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] 11 | [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")] 12 | public class KeycloakApiToken 13 | { 14 | private readonly DateTime _creationTime; 15 | 16 | /// 17 | /// Indicates whether the token is expired. 18 | /// 19 | public bool IsExpired => DateTime.UtcNow - _creationTime > ExpiresIn; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | public KeycloakApiToken(string accessToken, int expiresIn, int refreshExpiresIn, string refreshToken, string tokenType, int notBeforePolicy, string sessionState, string scope) 33 | { 34 | _creationTime = DateTime.UtcNow; 35 | AccessToken = accessToken; 36 | ExpiresIn = TimeSpan.FromSeconds(expiresIn - 10); 37 | RefreshExpiresIn = refreshExpiresIn; 38 | RefreshToken = refreshToken; 39 | TokenType = tokenType; 40 | NotBeforePolicy = notBeforePolicy; 41 | SessionState = sessionState; 42 | Scope = scope; 43 | } 44 | 45 | /// 46 | /// Access token. 47 | /// 48 | public string AccessToken { get; } 49 | 50 | /// 51 | /// Time until the token expires. 52 | /// 53 | public TimeSpan ExpiresIn { get; } 54 | 55 | /// 56 | /// Time until the refresh token expires. 57 | /// 58 | public int RefreshExpiresIn { get; } 59 | 60 | /// 61 | /// Refresh token. 62 | /// 63 | public string RefreshToken { get; } 64 | 65 | /// 66 | /// Token type. 67 | /// 68 | public string TokenType { get; } 69 | 70 | /// 71 | /// Not before policy. 72 | /// 73 | [JsonProperty("not-before-policy")] 74 | public int NotBeforePolicy { get; } 75 | 76 | /// 77 | /// Session state. 78 | /// 79 | public string SessionState { get; } 80 | 81 | /// 82 | /// Scope. 83 | /// 84 | public string Scope { get; } 85 | } 86 | } -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/Client/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | generated_code = true 3 | dotnet_analyzer_diagnostic.severity = none -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/ClientFactory/ApiClientFactory.cs: -------------------------------------------------------------------------------- 1 | using FS.Keycloak.RestApiClient.Authentication.Client; 2 | using FS.Keycloak.RestApiClient.Client; 3 | using System; 4 | 5 | namespace FS.Keycloak.RestApiClient.ClientFactory 6 | { 7 | /// 8 | /// Factory for creating API clients. 9 | /// 10 | public class ApiClientFactory 11 | { 12 | /// 13 | /// Create an API client. 14 | /// 15 | /// The type of the client. 16 | /// The HTTP client to use. 17 | public static TApiClient Create(AuthenticationHttpClient httpClient) where TApiClient : IApiAccessor 18 | => (TApiClient)Activator 19 | .CreateInstance( 20 | typeof(TApiClient), 21 | httpClient, 22 | new Configuration { BasePath = $"{httpClient.KeycloakUrl}" }, 23 | null 24 | ); 25 | } 26 | } 27 | 28 | namespace FS.Keycloak.RestApiClient.Client 29 | { 30 | /// 31 | [Obsolete("Use FS.Keycloak.RestApiClient.ClientFactory.ApiClientFactory instead.")] 32 | public class ApiClientFactory : ClientFactory.ApiClientFactory 33 | { } 34 | } -------------------------------------------------------------------------------- /FS.Keycloak.RestApiClient/src/FS.Keycloak.RestApiClient/ContractResolver/SnakeCaseContractResolver.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Serialization; 2 | 3 | namespace FS.Keycloak.RestApiClient.Client 4 | { 5 | internal class SnakeCaseContractResolver : DefaultContractResolver 6 | { 7 | public SnakeCaseContractResolver() 8 | => NamingStrategy = new SnakeCaseNamingStrategy(); 9 | } 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .NET / C# Keycloak.RestApiClient 2 | This is a .NET / C# REST API client 3 | 4 | * for the [Keycloak Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/index.html) 5 | * auto-generated by [OpenAPI Generator](https://openapi-generator.tech) 6 | * using the [OpenAPI definitions](https://www.keycloak.org/docs-api/latest/rest-api/openapi.json) provided by Keycloak 7 | 8 | ## Documentation for API Endpoints 9 | 10 | * Official documentation: [Keycloak Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/index.html) 11 | * Generated Swagger UI: [Swagger Editor](https://editor.swagger.io/?url=https://www.keycloak.org/docs-api/latest/rest-api/openapi.json) 12 | 13 | ## Keycloak versions supported 14 | 15 | | Keycloak.RestApiClient | Keycloak | 16 | | ---------------------- | -------- | 17 | | 26.n.n | 26.x.x | 18 | | ... | ... | 19 | | 19.n.n | 19.x.x | 20 | 21 | ## Frameworks supported 22 | 23 | - .NET Core >=1.0 24 | - .NET Framework >=4.6 25 | - Mono/Xamarin >=vNext 26 | 27 | ## Installation 28 | Install from NuGet package 29 | ```powershell 30 | Install-Package Schick.Keycloak.RestApiClient 31 | ``` 32 | 33 | ## Getting Started 34 | 35 | ### Method names 36 | 37 | Method names are humanized: 38 | 39 | `GET` on path `/{realm}/users` becomes `GetUsers(Async)` 40 | 41 | `GET` on path `/{realm}/identity-provider/providers/{provider_id}` becomes `GetIdentityProviderProvidersByProviderId(Async)` 42 | 43 | ### Authentication 44 | 45 | You can select authentication flow either by the username and password or by providing client ID and client secret. 46 | 47 | ### Sample code 48 | 49 | With authentication by username/password 50 | 51 | ```csharp 52 | using FS.Keycloak.RestApiClient.Api; 53 | using FS.Keycloak.RestApiClient.Authentication.ClientFactory; 54 | using FS.Keycloak.RestApiClient.Authentication.Flow; 55 | using FS.Keycloak.RestApiClient.ClientFactory; 56 | 57 | var credentials = new PasswordGrantFlow 58 | { 59 | KeycloakUrl = "https://", 60 | Realm = "", 61 | UserName = "", 62 | Password = "" 63 | }; 64 | 65 | using var httpClient = AuthenticationHttpClientFactory.Create(credentials); 66 | using var usersApi = ApiClientFactory.Create(httpClient); 67 | 68 | var users = await usersApi.GetUsersAsync(""); 69 | Console.WriteLine($"Users: {users.Count}"); 70 | ``` 71 | 72 | With authentication by client-id/client-secret 73 | 74 | ```csharp 75 | using FS.Keycloak.RestApiClient.Api; 76 | using FS.Keycloak.RestApiClient.Authentication.ClientFactory; 77 | using FS.Keycloak.RestApiClient.Authentication.Flow; 78 | using FS.Keycloak.RestApiClient.ClientFactory; 79 | 80 | var credentials = new ClientCredentialsFlow 81 | { 82 | KeycloakUrl = "https://", 83 | Realm = "", 84 | ClientId = "", 85 | ClientSecret = "" 86 | }; 87 | 88 | using var httpClient = AuthenticationHttpClientFactory.Create(credentials); 89 | using var usersApi = ApiClientFactory.Create(httpClient); 90 | 91 | var users = await usersApi.GetUsersAsync(""); 92 | Console.WriteLine($"Users: {users.Count}"); 93 | ``` 94 | 95 | ## Advanced Usage 96 | To use the API client with a HTTP proxy, setup a `System.Net.WebProxy` 97 | ```csharp 98 | Configuration c = new Configuration(); 99 | System.Net.WebProxy webProxy = new System.Net.WebProxy("http://myProxyUrl:80/"); 100 | webProxy.Credentials = System.Net.CredentialCache.DefaultCredentials; 101 | c.Proxy = webProxy; 102 | ``` 103 | 104 | ### Connections 105 | Each ApiClass (properly the ApiClient inside it) will create an instance of HttpClient. It will use that for the entire lifecycle and dispose it when called the Dispose method. 106 | 107 | To better manager the connections it's a common practice to reuse the HttpClient and HttpClientHandler (see [here](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests#issues-with-the-original-httpclient-class-available-in-net) for details). To use your own HttpClient instance just pass it to the ApiClass constructor. 108 | 109 | ```csharp 110 | HttpClientHandler yourHandler = new HttpClientHandler(); 111 | HttpClient yourHttpClient = new HttpClient(yourHandler); 112 | var api = new YourApiClass(yourHttpClient, yourHandler); 113 | ``` 114 | 115 | If you want to use an HttpClient and don't have access to the handler, for example in a DI context in Asp.net Core when using IHttpClientFactory. 116 | 117 | ```csharp 118 | HttpClient yourHttpClient = new HttpClient(); 119 | var api = new YourApiClass(yourHttpClient); 120 | ``` 121 | You'll loose some configuration settings, the features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. You need to either manually handle those in your setup of the HttpClient or they won't be available. 122 | 123 | Here an example of DI setup in a sample web project: 124 | 125 | ```csharp 126 | services.AddHttpClient(httpClient => new PetApi(httpClient)); 127 | ``` 128 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | :; set -eo pipefail 2 | :; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 3 | :; ${SCRIPT_DIR}/build.sh "$@" 4 | :; exit $? 5 | 6 | @ECHO OFF 7 | powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* 8 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 4 | [string[]]$BuildArguments 5 | ) 6 | 7 | Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" 8 | 9 | Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } 10 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 11 | 12 | ########################################################################### 13 | # CONFIGURATION 14 | ########################################################################### 15 | 16 | $BuildProjectFile = "$PSScriptRoot\build\_build.csproj" 17 | $TempDirectory = "$PSScriptRoot\\.nuke\temp" 18 | 19 | $DotNetGlobalFile = "$PSScriptRoot\\global.json" 20 | $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" 21 | $DotNetChannel = "STS" 22 | 23 | $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 24 | $env:DOTNET_NOLOGO = 1 25 | 26 | ########################################################################### 27 | # EXECUTION 28 | ########################################################################### 29 | 30 | function ExecSafe([scriptblock] $cmd) { 31 | & $cmd 32 | if ($LASTEXITCODE) { exit $LASTEXITCODE } 33 | } 34 | 35 | # If dotnet CLI is installed globally and it matches requested version, use for execution 36 | if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` 37 | $(dotnet --version) -and $LASTEXITCODE -eq 0) { 38 | $env:DOTNET_EXE = (Get-Command "dotnet").Path 39 | } 40 | else { 41 | # Download install script 42 | $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" 43 | New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null 44 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 45 | (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) 46 | 47 | # If global.json exists, load expected version 48 | if (Test-Path $DotNetGlobalFile) { 49 | $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) 50 | if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { 51 | $DotNetVersion = $DotNetGlobal.sdk.version 52 | } 53 | } 54 | 55 | # Install by channel or version 56 | $DotNetDirectory = "$TempDirectory\dotnet-win" 57 | if (!(Test-Path variable:DotNetVersion)) { 58 | ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } 59 | } else { 60 | ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } 61 | } 62 | $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" 63 | $env:PATH = "$DotNetDirectory;$env:PATH" 64 | } 65 | 66 | Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" 67 | 68 | if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { 69 | & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null 70 | & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null 71 | } 72 | 73 | ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } 74 | ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } 75 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bash --version 2>&1 | head -n 1 4 | 5 | set -eo pipefail 6 | SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | 8 | ########################################################################### 9 | # CONFIGURATION 10 | ########################################################################### 11 | 12 | BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" 13 | TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" 14 | 15 | DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" 16 | DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" 17 | DOTNET_CHANNEL="STS" 18 | 19 | export DOTNET_CLI_TELEMETRY_OPTOUT=1 20 | export DOTNET_NOLOGO=1 21 | 22 | ########################################################################### 23 | # EXECUTION 24 | ########################################################################### 25 | 26 | function FirstJsonValue { 27 | perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" 28 | } 29 | 30 | # If dotnet CLI is installed globally and it matches requested version, use for execution 31 | if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then 32 | export DOTNET_EXE="$(command -v dotnet)" 33 | else 34 | # Download install script 35 | DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" 36 | mkdir -p "$TEMP_DIRECTORY" 37 | curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" 38 | chmod +x "$DOTNET_INSTALL_FILE" 39 | 40 | # If global.json exists, load expected version 41 | if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then 42 | DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") 43 | if [[ "$DOTNET_VERSION" == "" ]]; then 44 | unset DOTNET_VERSION 45 | fi 46 | fi 47 | 48 | # Install by channel or version 49 | DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" 50 | if [[ -z ${DOTNET_VERSION+x} ]]; then 51 | "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path 52 | else 53 | "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path 54 | fi 55 | export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" 56 | export PATH="$DOTNET_DIRECTORY:$PATH" 57 | fi 58 | 59 | echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" 60 | 61 | if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then 62 | "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true 63 | "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true 64 | fi 65 | 66 | "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet 67 | "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" 68 | -------------------------------------------------------------------------------- /build/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_style_qualification_for_field = false:warning 3 | dotnet_style_qualification_for_property = false:warning 4 | dotnet_style_qualification_for_method = false:warning 5 | dotnet_style_qualification_for_event = false:warning 6 | dotnet_style_require_accessibility_modifiers = never:warning 7 | 8 | csharp_style_expression_bodied_methods = true:silent 9 | csharp_style_expression_bodied_properties = true:warning 10 | csharp_style_expression_bodied_indexers = true:warning 11 | csharp_style_expression_bodied_accessors = true:warning 12 | -------------------------------------------------------------------------------- /build/Build.cs: -------------------------------------------------------------------------------- 1 | using Nuke.Common; 2 | using Nuke.Common.Git; 3 | using Nuke.Common.IO; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | 7 | [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] 8 | class Build : NukeBuild 9 | { 10 | [GitRepository] 11 | private readonly GitRepository Repository = default!; 12 | 13 | [Parameter] 14 | private string Version = "0.0.0"; 15 | 16 | [Parameter("Path or download URL for Open API definition to build client for")] 17 | private readonly string OpenApiJson = "https://www.keycloak.org/docs-api/latest/rest-api/openapi.json"; 18 | 19 | [Parameter] 20 | private AbsolutePath PublishFolder = default!; 21 | 22 | [Parameter, Secret] 23 | private readonly string? NuGetApiKey; 24 | 25 | protected override void OnBuildInitialized() 26 | { 27 | base.OnBuildInitialized(); 28 | 29 | if (Version == "0.0.0" && Repository.Tags.Count > 0) 30 | Version = Repository.Tags.First(); 31 | 32 | // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract 33 | if (PublishFolder == null) 34 | PublishFolder = RootDirectory / $"Keycloak.RestApiClient.{Version}"; 35 | } 36 | 37 | public static int Main() 38 | => Execute(x => x.BuildClient); 39 | 40 | public Target Restore => target => target 41 | .Executes(() => 42 | { 43 | Npm.Restore(BuildPaths.Generator.Path); 44 | }); 45 | 46 | public Target ProvideOpenApiSpec => target => target 47 | .Executes(() => 48 | { 49 | OpenApiSpec.Download(OpenApiJson, BuildPaths.OpenApiJson.Downloaded); 50 | OpenApiSpec.Reformat(BuildPaths.OpenApiJson.Downloaded, BuildPaths.OpenApiJson.Formatted); 51 | OpenApiSpec.ApplyFixes(BuildPaths.OpenApiJson.Formatted, BuildPaths.OpenApiJson.Fixed); 52 | OpenApiSpec.EscapeForXmlDocumentation(BuildPaths.OpenApiJson.Fixed, BuildPaths.OpenApiJson.Escaped); 53 | OpenApiSpec.HumanizeActionNames(BuildPaths.OpenApiJson.Escaped, BuildPaths.OpenApiJson.Humanized); 54 | }); 55 | 56 | public Target GenerateClient => target => target 57 | .DependsOn(Restore) 58 | .DependsOn(ProvideOpenApiSpec) 59 | .Executes(() => 60 | { 61 | ClientGenerator.GenerateClient(BuildPaths.OpenApiJson.Humanized, BuildPaths.Client.Path); 62 | }); 63 | 64 | public Target BuildClient => target => target 65 | .DependsOn(GenerateClient) 66 | .Executes(() => 67 | { 68 | Project.CopyReadme(BuildPaths.Readme, BuildPaths.Client.Readme); 69 | Project.AdjustImports(BuildPaths.Client.ProjectFile); 70 | Project.Build(BuildPaths.Client.ProjectFile, Version); 71 | Project.PackNuget(BuildPaths.Client.ProjectFile, Version, PublishFolder); 72 | }); 73 | 74 | public Target PublishClient => target => target 75 | .DependsOn(BuildClient) 76 | .Executes(() => 77 | { 78 | Project.PushNuget(PublishFolder, NuGetApiKey); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /build/Configuration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq; 4 | using Nuke.Common.Tooling; 5 | 6 | [TypeConverter(typeof(TypeConverter))] 7 | public class Configuration : Enumeration 8 | { 9 | public static Configuration Debug = new Configuration { Value = nameof(Debug) }; 10 | public static Configuration Release = new Configuration { Value = nameof(Release) }; 11 | 12 | public static implicit operator string(Configuration configuration) 13 | { 14 | return configuration.Value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Build": { 4 | "commandName": "Project", 5 | "commandLineArgs": "BuildClient" 6 | }, 7 | "Publish": { 8 | "commandName": "Project", 9 | "commandLineArgs": "PublishClient" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /build/_build.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 7 | CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006 8 | .. 9 | .. 10 | 1 11 | false 12 | enable 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /build/_build.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | DO_NOT_SHOW 3 | DO_NOT_SHOW 4 | DO_NOT_SHOW 5 | DO_NOT_SHOW 6 | DO_NOT_SHOW 7 | True 8 | True 9 | True 10 | Implicit 11 | Implicit 12 | ExpressionBody 13 | 0 14 | NEXT_LINE 15 | True 16 | False 17 | 120 18 | IF_OWNER_IS_SINGLE_LINE 19 | WRAP_IF_LONG 20 | False 21 | <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> 22 | <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> 23 | <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> 24 | <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></Policy> 25 | True 26 | True 27 | True 28 | True 29 | True 30 | True 31 | True 32 | True 33 | True 34 | True 35 | -------------------------------------------------------------------------------- /build/_build.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35521.163 d17.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "_build", "_build.csproj", "{DFF55D8A-41AC-4BA6-8960-63C2A10DB3A2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {DFF55D8A-41AC-4BA6-8960-63C2A10DB3A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {DFF55D8A-41AC-4BA6-8960-63C2A10DB3A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {DFF55D8A-41AC-4BA6-8960-63C2A10DB3A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {DFF55D8A-41AC-4BA6-8960-63C2A10DB3A2}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /build/openapi-generator/openapi-generator.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "caseInsensitiveResponseHeaders": true, 3 | "library": "httpclient", 4 | "netCoreProjectFile": true, 5 | "targetFramework": "netstandard2.0", 6 | "packageName": "FS.Keycloak.RestApiClient", 7 | "useDateTimeOffset": false, 8 | "conditionalSerialization": true, 9 | "nullableReferenceTypes": false, 10 | "optionalEmitDefaultValues": true 11 | } -------------------------------------------------------------------------------- /build/openapi-generator/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "7.5.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /build/openapi-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build", 3 | "version": "1.0.0", 4 | "description": "Internal build only", 5 | "main": "index.js", 6 | "author": "", 7 | "license": "ISC", 8 | "devDependencies": { 9 | "@openapitools/openapi-generator-cli": "^2.13.2" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /build/openapi-generator/templates/api.mustache: -------------------------------------------------------------------------------- 1 | {{>partial_header}} 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.Mime; 10 | using {{packageName}}.Client; 11 | {{#hasImport}}using {{packageName}}.{{modelPackage}}; 12 | {{/hasImport}} 13 | 14 | namespace {{packageName}}.{{apiPackage}} 15 | { 16 | {{#operations}} 17 | 18 | /// 19 | /// Represents a collection of functions to interact with the API endpoints 20 | /// 21 | {{>visibility}} interface {{interfacePrefix}}{{classname}}Sync : IApiAccessor 22 | { 23 | #region Synchronous Operations 24 | {{#operation}} 25 | /// 26 | /// {{summary}} 27 | /// 28 | {{#notes}} 29 | /// 30 | /// {{.}} 31 | /// 32 | {{/notes}} 33 | /// Thrown when fails to make API call 34 | {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 35 | {{/allParams}}/// {{returnType}} 36 | {{#isDeprecated}} 37 | [Obsolete] 38 | {{/isDeprecated}} 39 | {{{returnType}}}{{^returnType}}void{{/returnType}} {{vendorExtensions.x-csharp-action}}({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}); 40 | 41 | /// 42 | /// {{summary}} 43 | /// 44 | /// 45 | /// {{notes}} 46 | /// 47 | /// Thrown when fails to make API call 48 | {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 49 | {{/allParams}}/// ApiResponse of {{returnType}}{{^returnType}}Object(void){{/returnType}} 50 | {{#isDeprecated}} 51 | [Obsolete] 52 | {{/isDeprecated}} 53 | ApiResponse<{{{returnType}}}{{^returnType}}Object{{/returnType}}> {{vendorExtensions.x-csharp-action}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}); 54 | {{/operation}} 55 | #endregion Synchronous Operations 56 | } 57 | 58 | {{#supportsAsync}} 59 | /// 60 | /// Represents a collection of functions to interact with the API endpoints 61 | /// 62 | {{>visibility}} interface {{interfacePrefix}}{{classname}}Async : IApiAccessor 63 | { 64 | #region Asynchronous Operations 65 | {{#operation}} 66 | /// 67 | /// {{summary}} 68 | /// 69 | /// 70 | /// {{notes}} 71 | /// 72 | /// Thrown when fails to make API call 73 | {{#allParams}} 74 | /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 75 | {{/allParams}} 76 | /// Cancellation Token to cancel the request. 77 | /// Task of {{returnType}}{{^returnType}}void{{/returnType}} 78 | {{#isDeprecated}} 79 | [Obsolete] 80 | {{/isDeprecated}} 81 | {{#returnType}}System.Threading.Tasks.Task<{{{.}}}>{{/returnType}}{{^returnType}}System.Threading.Tasks.Task{{/returnType}} {{vendorExtensions.x-csharp-action}}Async({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 82 | 83 | /// 84 | /// {{summary}} 85 | /// 86 | /// 87 | /// {{notes}} 88 | /// 89 | /// Thrown when fails to make API call 90 | {{#allParams}} 91 | /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 92 | {{/allParams}} 93 | /// Cancellation Token to cancel the request. 94 | /// Task of ApiResponse{{#returnType}} ({{.}}){{/returnType}} 95 | {{#isDeprecated}} 96 | [Obsolete] 97 | {{/isDeprecated}} 98 | System.Threading.Tasks.Task> {{vendorExtensions.x-csharp-action}}WithHttpInfoAsync({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); 99 | {{/operation}} 100 | #endregion Asynchronous Operations 101 | } 102 | {{/supportsAsync}} 103 | 104 | /// 105 | /// Represents a collection of functions to interact with the API endpoints 106 | /// 107 | {{>visibility}} interface {{interfacePrefix}}{{classname}} : {{interfacePrefix}}{{classname}}Sync{{#supportsAsync}}, {{interfacePrefix}}{{classname}}Async{{/supportsAsync}} 108 | { 109 | 110 | } 111 | 112 | /// 113 | /// Represents a collection of functions to interact with the API endpoints 114 | /// 115 | {{>visibility}} partial class {{classname}} : IDisposable, {{interfacePrefix}}{{classname}} 116 | { 117 | private {{packageName}}.Client.ExceptionFactory _exceptionFactory = (name, response) => null; 118 | 119 | /// 120 | /// Initializes a new instance of the class. 121 | /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. 122 | /// It's better to reuse the HttpClient and HttpClientHandler. 123 | /// 124 | /// 125 | public {{classname}}() : this((string)null) 126 | { 127 | } 128 | 129 | /// 130 | /// Initializes a new instance of the class. 131 | /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. 132 | /// It's better to reuse the HttpClient and HttpClientHandler. 133 | /// 134 | /// The target service's base path in URL format. 135 | /// 136 | /// 137 | public {{classname}}(string basePath) 138 | { 139 | this.Configuration = {{packageName}}.Client.Configuration.MergeConfigurations( 140 | {{packageName}}.Client.GlobalConfiguration.Instance, 141 | new {{packageName}}.Client.Configuration { BasePath = basePath } 142 | ); 143 | this.ApiClient = new {{packageName}}.Client.ApiClient(this.Configuration.BasePath); 144 | this.Client = this.ApiClient; 145 | {{#supportsAsync}} 146 | this.AsynchronousClient = this.ApiClient; 147 | {{/supportsAsync}} 148 | this.ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; 149 | } 150 | 151 | /// 152 | /// Initializes a new instance of the class using Configuration object. 153 | /// **IMPORTANT** This will also create an instance of HttpClient, which is less than ideal. 154 | /// It's better to reuse the HttpClient and HttpClientHandler. 155 | /// 156 | /// An instance of Configuration. 157 | /// 158 | /// 159 | public {{classname}}({{packageName}}.Client.Configuration configuration) 160 | { 161 | if (configuration == null) throw new ArgumentNullException("configuration"); 162 | 163 | this.Configuration = {{packageName}}.Client.Configuration.MergeConfigurations( 164 | {{packageName}}.Client.GlobalConfiguration.Instance, 165 | configuration 166 | ); 167 | this.ApiClient = new {{packageName}}.Client.ApiClient(this.Configuration.BasePath); 168 | this.Client = this.ApiClient; 169 | {{#supportsAsync}} 170 | this.AsynchronousClient = this.ApiClient; 171 | {{/supportsAsync}} 172 | ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; 173 | } 174 | 175 | /// 176 | /// Initializes a new instance of the class. 177 | /// 178 | /// An instance of HttpClient. 179 | /// An optional instance of HttpClientHandler that is used by HttpClient. 180 | /// 181 | /// 182 | /// 183 | /// Some configuration settings will not be applied without passing an HttpClientHandler. 184 | /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. 185 | /// 186 | public {{classname}}(HttpClient client, HttpClientHandler handler = null) : this(client, (string)null, handler) 187 | { 188 | } 189 | 190 | /// 191 | /// Initializes a new instance of the class. 192 | /// 193 | /// An instance of HttpClient. 194 | /// The target service's base path in URL format. 195 | /// An optional instance of HttpClientHandler that is used by HttpClient. 196 | /// 197 | /// 198 | /// 199 | /// 200 | /// Some configuration settings will not be applied without passing an HttpClientHandler. 201 | /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. 202 | /// 203 | public {{classname}}(HttpClient client, string basePath, HttpClientHandler handler = null) 204 | { 205 | if (client == null) throw new ArgumentNullException("client"); 206 | 207 | this.Configuration = {{packageName}}.Client.Configuration.MergeConfigurations( 208 | {{packageName}}.Client.GlobalConfiguration.Instance, 209 | new {{packageName}}.Client.Configuration { BasePath = basePath } 210 | ); 211 | this.ApiClient = new {{packageName}}.Client.ApiClient(client, this.Configuration.BasePath, handler); 212 | this.Client = this.ApiClient; 213 | {{#supportsAsync}} 214 | this.AsynchronousClient = this.ApiClient; 215 | {{/supportsAsync}} 216 | this.ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; 217 | } 218 | 219 | /// 220 | /// Initializes a new instance of the class using Configuration object. 221 | /// 222 | /// An instance of HttpClient. 223 | /// An instance of Configuration. 224 | /// An optional instance of HttpClientHandler that is used by HttpClient. 225 | /// 226 | /// 227 | /// 228 | /// Some configuration settings will not be applied without passing an HttpClientHandler. 229 | /// The features affected are: Setting and Retrieving Cookies, Client Certificates, Proxy settings. 230 | /// 231 | public {{classname}}(HttpClient client, {{packageName}}.Client.Configuration configuration, HttpClientHandler handler = null) 232 | { 233 | if (configuration == null) throw new ArgumentNullException("configuration"); 234 | if (client == null) throw new ArgumentNullException("client"); 235 | 236 | this.Configuration = {{packageName}}.Client.Configuration.MergeConfigurations( 237 | {{packageName}}.Client.GlobalConfiguration.Instance, 238 | configuration 239 | ); 240 | this.ApiClient = new {{packageName}}.Client.ApiClient(client, this.Configuration.BasePath, handler); 241 | this.Client = this.ApiClient; 242 | {{#supportsAsync}} 243 | this.AsynchronousClient = this.ApiClient; 244 | {{/supportsAsync}} 245 | ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; 246 | } 247 | 248 | /// 249 | /// Initializes a new instance of the class 250 | /// using a Configuration object and client instance. 251 | /// 252 | /// The client interface for synchronous API access.{{#supportsAsync}} 253 | /// The client interface for asynchronous API access.{{/supportsAsync}} 254 | /// The configuration object. 255 | /// 256 | public {{classname}}({{packageName}}.Client.ISynchronousClient client, {{#supportsAsync}}{{packageName}}.Client.IAsynchronousClient asyncClient, {{/supportsAsync}}{{packageName}}.Client.IReadableConfiguration configuration) 257 | { 258 | if (client == null) throw new ArgumentNullException("client"); 259 | {{#supportsAsync}} 260 | if (asyncClient == null) throw new ArgumentNullException("asyncClient"); 261 | {{/supportsAsync}} 262 | if (configuration == null) throw new ArgumentNullException("configuration"); 263 | 264 | this.Client = client; 265 | {{#supportsAsync}} 266 | this.AsynchronousClient = asyncClient; 267 | {{/supportsAsync}} 268 | this.Configuration = configuration; 269 | this.ExceptionFactory = {{packageName}}.Client.Configuration.DefaultExceptionFactory; 270 | } 271 | 272 | /// 273 | /// Disposes resources if they were created by us 274 | /// 275 | public void Dispose() 276 | { 277 | this.ApiClient?.Dispose(); 278 | } 279 | 280 | /// 281 | /// Holds the ApiClient if created 282 | /// 283 | public {{packageName}}.Client.ApiClient ApiClient { get; set; } = null; 284 | 285 | {{#supportsAsync}} 286 | /// 287 | /// The client for accessing this underlying API asynchronously. 288 | /// 289 | public {{packageName}}.Client.IAsynchronousClient AsynchronousClient { get; set; } 290 | {{/supportsAsync}} 291 | 292 | /// 293 | /// The client for accessing this underlying API synchronously. 294 | /// 295 | public {{packageName}}.Client.ISynchronousClient Client { get; set; } 296 | 297 | /// 298 | /// Gets the base path of the API client. 299 | /// 300 | /// The base path 301 | public string GetBasePath() 302 | { 303 | return this.Configuration.BasePath; 304 | } 305 | 306 | /// 307 | /// Gets or sets the configuration object 308 | /// 309 | /// An instance of the Configuration 310 | public {{packageName}}.Client.IReadableConfiguration Configuration { get; set; } 311 | 312 | /// 313 | /// Provides a factory method hook for the creation of exceptions. 314 | /// 315 | public {{packageName}}.Client.ExceptionFactory ExceptionFactory 316 | { 317 | get 318 | { 319 | if (_exceptionFactory != null && _exceptionFactory.GetInvocationList().Length > 1) 320 | { 321 | throw new InvalidOperationException("Multicast delegate for ExceptionFactory is unsupported."); 322 | } 323 | return _exceptionFactory; 324 | } 325 | set { _exceptionFactory = value; } 326 | } 327 | 328 | {{#operation}} 329 | /// 330 | /// {{summary}} {{notes}} 331 | /// 332 | /// Thrown when fails to make API call 333 | {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 334 | {{/allParams}}/// {{returnType}} 335 | {{#isDeprecated}} 336 | [Obsolete] 337 | {{/isDeprecated}} 338 | public {{{returnType}}}{{^returnType}}void{{/returnType}} {{vendorExtensions.x-csharp-action}}({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) 339 | { 340 | {{#returnType}}{{packageName}}.Client.ApiResponse<{{{returnType}}}> localVarResponse = {{vendorExtensions.x-csharp-action}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); 341 | return localVarResponse.Data;{{/returnType}}{{^returnType}}{{vendorExtensions.x-csharp-action}}WithHttpInfo({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});{{/returnType}} 342 | } 343 | 344 | /// 345 | /// {{summary}} {{notes}} 346 | /// 347 | /// Thrown when fails to make API call 348 | {{#allParams}}/// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 349 | {{/allParams}}/// ApiResponse of {{returnType}}{{^returnType}}Object(void){{/returnType}} 350 | {{#isDeprecated}} 351 | [Obsolete] 352 | {{/isDeprecated}} 353 | public {{packageName}}.Client.ApiResponse<{{{returnType}}}{{^returnType}}Object{{/returnType}}> {{vendorExtensions.x-csharp-action}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) 354 | { 355 | {{#allParams}} 356 | {{#required}} 357 | {{^vendorExtensions.x-csharp-value-type}} 358 | // verify the required parameter '{{paramName}}' is set 359 | if ({{paramName}} == null) 360 | throw new {{packageName}}.Client.ApiException(400, "Missing required parameter '{{paramName}}' when calling {{classname}}->{{vendorExtensions.x-csharp-action}}"); 361 | 362 | {{/vendorExtensions.x-csharp-value-type}} 363 | {{/required}} 364 | {{/allParams}} 365 | {{packageName}}.Client.RequestOptions localVarRequestOptions = new {{packageName}}.Client.RequestOptions(); 366 | 367 | string[] _contentTypes = new string[] { 368 | {{#consumes}} 369 | "{{{mediaType}}}"{{^-last}},{{/-last}} 370 | {{/consumes}} 371 | }; 372 | 373 | // to determine the Accept header 374 | string[] _accepts = new string[] { 375 | {{#produces}} 376 | "{{{mediaType}}}"{{^-last}},{{/-last}} 377 | {{/produces}} 378 | }; 379 | 380 | var localVarContentType = {{packageName}}.Client.ClientUtils.SelectHeaderContentType(_contentTypes); 381 | if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); 382 | 383 | var localVarAccept = {{packageName}}.Client.ClientUtils.SelectHeaderAccept(_accepts); 384 | if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); 385 | 386 | {{#pathParams}} 387 | {{#required}} 388 | localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter 389 | {{/required}} 390 | {{^required}} 391 | if ({{paramName}} != null) 392 | { 393 | localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter 394 | } 395 | {{/required}} 396 | {{/pathParams}} 397 | {{#queryParams}} 398 | {{#required}} 399 | {{#isDeepObject}} 400 | {{#items.vars}} 401 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}}.{{name}})); 402 | {{/items.vars}} 403 | {{^items}} 404 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("deepObject", "{{baseName}}", {{paramName}})); 405 | {{/items}} 406 | {{/isDeepObject}} 407 | {{^isDeepObject}} 408 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); 409 | {{/isDeepObject}} 410 | {{/required}} 411 | {{^required}} 412 | if ({{paramName}} != null) 413 | { 414 | {{#isDeepObject}} 415 | {{#items.vars}} 416 | if ({{paramName}}.{{name}} != null) 417 | { 418 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}}.{{name}})); 419 | } 420 | {{/items.vars}} 421 | {{^items}} 422 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("deepObject", "{{baseName}}", {{paramName}})); 423 | {{/items}} 424 | {{/isDeepObject}} 425 | {{^isDeepObject}} 426 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); 427 | {{/isDeepObject}} 428 | } 429 | {{/required}} 430 | {{/queryParams}} 431 | {{#headerParams}} 432 | {{#required}} 433 | localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter 434 | {{/required}} 435 | {{^required}} 436 | if ({{paramName}} != null) 437 | { 438 | localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter 439 | } 440 | {{/required}} 441 | {{/headerParams}} 442 | {{#formParams}} 443 | {{#required}} 444 | {{#isFile}} 445 | localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); 446 | {{/isFile}} 447 | {{^isFile}} 448 | localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter 449 | {{/isFile}} 450 | {{/required}} 451 | {{^required}} 452 | if ({{paramName}} != null) 453 | { 454 | {{#isFile}} 455 | localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); 456 | {{/isFile}} 457 | {{^isFile}} 458 | localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter 459 | {{/isFile}} 460 | } 461 | {{/required}} 462 | {{/formParams}} 463 | {{#bodyParam}} 464 | localVarRequestOptions.Data = {{paramName}}; 465 | {{/bodyParam}} 466 | 467 | {{#authMethods}} 468 | // authentication ({{name}}) required 469 | {{#isApiKey}} 470 | {{#isKeyInCookie}} 471 | // cookie parameter support 472 | if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) 473 | { 474 | localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); 475 | } 476 | {{/isKeyInCookie}} 477 | {{#isKeyInHeader}} 478 | if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) 479 | { 480 | localVarRequestOptions.HeaderParameters.Add("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")); 481 | } 482 | {{/isKeyInHeader}} 483 | {{#isKeyInQuery}} 484 | if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) 485 | { 486 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("", "{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); 487 | } 488 | {{/isKeyInQuery}} 489 | {{/isApiKey}} 490 | {{#isBasicBasic}} 491 | // http basic authentication required 492 | if (!string.IsNullOrEmpty(this.Configuration.Username) || !string.IsNullOrEmpty(this.Configuration.Password) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) 493 | { 494 | localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password)); 495 | } 496 | {{/isBasicBasic}} 497 | {{#isBasicBearer}} 498 | // bearer authentication required 499 | if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) 500 | { 501 | localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); 502 | } 503 | {{/isBasicBearer}} 504 | {{#isOAuth}} 505 | // oauth required 506 | if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) 507 | { 508 | localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); 509 | } 510 | {{/isOAuth}} 511 | {{#isHttpSignature}} 512 | if (this.Configuration.HttpSigningConfiguration != null) 513 | { 514 | var HttpSigningHeaders = this.Configuration.HttpSigningConfiguration.GetHttpSignedHeader(this.Configuration.BasePath, "{{{httpMethod}}}", "{{{path}}}", localVarRequestOptions); 515 | foreach (var headerItem in HttpSigningHeaders) 516 | { 517 | if (localVarRequestOptions.HeaderParameters.ContainsKey(headerItem.Key)) 518 | { 519 | localVarRequestOptions.HeaderParameters[headerItem.Key] = new List() { headerItem.Value }; 520 | } 521 | else 522 | { 523 | localVarRequestOptions.HeaderParameters.Add(headerItem.Key, headerItem.Value); 524 | } 525 | } 526 | } 527 | {{/isHttpSignature}} 528 | {{/authMethods}} 529 | 530 | // make the HTTP request 531 | var localVarResponse = this.Client.{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}}<{{{returnType}}}{{^returnType}}Object{{/returnType}}>("{{{path}}}", localVarRequestOptions, this.Configuration); 532 | 533 | if (this.ExceptionFactory != null) 534 | { 535 | Exception _exception = this.ExceptionFactory("{{vendorExtensions.x-csharp-action}}", localVarResponse); 536 | if (_exception != null) throw _exception; 537 | } 538 | 539 | return localVarResponse; 540 | } 541 | 542 | {{#supportsAsync}} 543 | /// 544 | /// {{summary}} {{notes}} 545 | /// 546 | /// Thrown when fails to make API call 547 | {{#allParams}} 548 | /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 549 | {{/allParams}} 550 | /// Cancellation Token to cancel the request. 551 | /// Task of {{returnType}}{{^returnType}}void{{/returnType}} 552 | {{#isDeprecated}} 553 | [Obsolete] 554 | {{/isDeprecated}} 555 | {{#returnType}}public async System.Threading.Tasks.Task<{{{.}}}>{{/returnType}}{{^returnType}}public async System.Threading.Tasks.Task{{/returnType}} {{vendorExtensions.x-csharp-action}}Async({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) 556 | { 557 | {{#returnType}}{{packageName}}.Client.ApiResponse<{{{returnType}}}> localVarResponse = await {{vendorExtensions.x-csharp-action}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false); 558 | return localVarResponse.Data;{{/returnType}}{{^returnType}}await {{vendorExtensions.x-csharp-action}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false);{{/returnType}} 559 | } 560 | 561 | /// 562 | /// {{summary}} {{notes}} 563 | /// 564 | /// Thrown when fails to make API call 565 | {{#allParams}} 566 | /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}{{#isDeprecated}} (deprecated){{/isDeprecated}} 567 | {{/allParams}} 568 | /// Cancellation Token to cancel the request. 569 | /// Task of ApiResponse{{#returnType}} ({{.}}){{/returnType}} 570 | {{#isDeprecated}} 571 | [Obsolete] 572 | {{/isDeprecated}} 573 | public async System.Threading.Tasks.Task<{{packageName}}.Client.ApiResponse<{{{returnType}}}{{^returnType}}Object{{/returnType}}>> {{vendorExtensions.x-csharp-action}}WithHttpInfoAsync({{#allParams}}{{{dataType}}} {{paramName}}{{^required}}{{#optionalMethodArgument}} = default({{{dataType}}}){{/optionalMethodArgument}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) 574 | { 575 | {{#allParams}} 576 | {{#required}} 577 | {{^vendorExtensions.x-csharp-value-type}} 578 | // verify the required parameter '{{paramName}}' is set 579 | if ({{paramName}} == null) 580 | throw new {{packageName}}.Client.ApiException(400, "Missing required parameter '{{paramName}}' when calling {{classname}}->{{vendorExtensions.x-csharp-action}}"); 581 | 582 | {{/vendorExtensions.x-csharp-value-type}} 583 | {{/required}} 584 | {{/allParams}} 585 | 586 | {{packageName}}.Client.RequestOptions localVarRequestOptions = new {{packageName}}.Client.RequestOptions(); 587 | 588 | string[] _contentTypes = new string[] { 589 | {{#consumes}} 590 | "{{{mediaType}}}"{{^-last}}, {{/-last}} 591 | {{/consumes}} 592 | }; 593 | 594 | // to determine the Accept header 595 | string[] _accepts = new string[] { 596 | {{#produces}} 597 | "{{{mediaType}}}"{{^-last}},{{/-last}} 598 | {{/produces}} 599 | }; 600 | 601 | 602 | var localVarContentType = {{packageName}}.Client.ClientUtils.SelectHeaderContentType(_contentTypes); 603 | if (localVarContentType != null) localVarRequestOptions.HeaderParameters.Add("Content-Type", localVarContentType); 604 | 605 | var localVarAccept = {{packageName}}.Client.ClientUtils.SelectHeaderAccept(_accepts); 606 | if (localVarAccept != null) localVarRequestOptions.HeaderParameters.Add("Accept", localVarAccept); 607 | 608 | {{#constantParams}} 609 | {{#isPathParam}} 610 | // Set client side default value of Path Param "{{baseName}}". 611 | localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{#_enum}}"{{{.}}}"{{/_enum}})); // Constant path parameter 612 | {{/isPathParam}} 613 | {{/constantParams}} 614 | {{#pathParams}} 615 | {{#required}} 616 | localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter 617 | {{/required}} 618 | {{^required}} 619 | if ({{paramName}} != null) 620 | { 621 | localVarRequestOptions.PathParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // path parameter 622 | } 623 | {{/required}} 624 | {{/pathParams}} 625 | {{#constantParams}} 626 | {{#isQueryParam}} 627 | // Set client side default value of Query Param "{{baseName}}". 628 | localVarRequestOptions.QueryParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{#_enum}}"{{{.}}}"{{/_enum}})); // Constant query parameter 629 | {{/isQueryParam}} 630 | {{/constantParams}} 631 | {{#queryParams}} 632 | {{#required}} 633 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); 634 | {{/required}} 635 | {{^required}} 636 | if ({{paramName}} != null) 637 | { 638 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("{{collectionFormat}}", "{{baseName}}", {{paramName}})); 639 | } 640 | {{/required}} 641 | {{/queryParams}} 642 | {{#constantParams}} 643 | {{#isHeaderParam}} 644 | // Set client side default value of Header Param "{{baseName}}". 645 | localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{#_enum}}"{{{.}}}"{{/_enum}})); // Constant header parameter 646 | {{/isHeaderParam}} 647 | {{/constantParams}} 648 | {{#headerParams}} 649 | {{#required}} 650 | localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter 651 | {{/required}} 652 | {{^required}} 653 | if ({{paramName}} != null) 654 | { 655 | localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter 656 | } 657 | {{/required}} 658 | {{/headerParams}} 659 | {{#formParams}} 660 | {{#required}} 661 | {{#isFile}} 662 | localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); 663 | {{/isFile}} 664 | {{^isFile}} 665 | localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter 666 | {{/isFile}} 667 | {{/required}} 668 | {{^required}} 669 | if ({{paramName}} != null) 670 | { 671 | {{#isFile}} 672 | localVarRequestOptions.FileParameters.Add("{{baseName}}", {{paramName}}); 673 | {{/isFile}} 674 | {{^isFile}} 675 | localVarRequestOptions.FormParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // form parameter 676 | {{/isFile}} 677 | } 678 | {{/required}} 679 | {{/formParams}} 680 | {{#bodyParam}} 681 | localVarRequestOptions.Data = {{paramName}}; 682 | {{/bodyParam}} 683 | 684 | {{#authMethods}} 685 | // authentication ({{name}}) required 686 | {{#isApiKey}} 687 | {{#isKeyInCookie}} 688 | // cookie parameter support 689 | if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) 690 | { 691 | localVarRequestOptions.Cookies.Add(new Cookie("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); 692 | } 693 | {{/isKeyInCookie}} 694 | {{#isKeyInHeader}} 695 | if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) 696 | { 697 | localVarRequestOptions.HeaderParameters.Add("{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}")); 698 | } 699 | {{/isKeyInHeader}} 700 | {{#isKeyInQuery}} 701 | if (!string.IsNullOrEmpty(this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))) 702 | { 703 | localVarRequestOptions.QueryParameters.Add({{packageName}}.Client.ClientUtils.ParameterToMultiMap("", "{{keyParamName}}", this.Configuration.GetApiKeyWithPrefix("{{keyParamName}}"))); 704 | } 705 | {{/isKeyInQuery}} 706 | {{/isApiKey}} 707 | {{#isBasic}} 708 | {{#isBasicBasic}} 709 | // http basic authentication required 710 | if (!string.IsNullOrEmpty(this.Configuration.Username) || !string.IsNullOrEmpty(this.Configuration.Password) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) 711 | { 712 | localVarRequestOptions.HeaderParameters.Add("Authorization", "Basic " + {{packageName}}.Client.ClientUtils.Base64Encode(this.Configuration.Username + ":" + this.Configuration.Password)); 713 | } 714 | {{/isBasicBasic}} 715 | {{#isBasicBearer}} 716 | // bearer authentication required 717 | if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) 718 | { 719 | localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); 720 | } 721 | {{/isBasicBearer}} 722 | {{/isBasic}} 723 | {{#isOAuth}} 724 | // oauth required 725 | if (!string.IsNullOrEmpty(this.Configuration.AccessToken) && !localVarRequestOptions.HeaderParameters.ContainsKey("Authorization")) 726 | { 727 | localVarRequestOptions.HeaderParameters.Add("Authorization", "Bearer " + this.Configuration.AccessToken); 728 | } 729 | {{/isOAuth}} 730 | {{#isHttpSignature}} 731 | if (this.Configuration.HttpSigningConfiguration != null) 732 | { 733 | var HttpSigningHeaders = this.Configuration.HttpSigningConfiguration.GetHttpSignedHeader(this.Configuration.BasePath, "{{{httpMethod}}}", "{{{path}}}", localVarRequestOptions); 734 | foreach (var headerItem in HttpSigningHeaders) 735 | { 736 | if (localVarRequestOptions.HeaderParameters.ContainsKey(headerItem.Key)) 737 | { 738 | localVarRequestOptions.HeaderParameters[headerItem.Key] = new List() { headerItem.Value }; 739 | } 740 | else 741 | { 742 | localVarRequestOptions.HeaderParameters.Add(headerItem.Key, headerItem.Value); 743 | } 744 | } 745 | } 746 | {{/isHttpSignature}} 747 | {{/authMethods}} 748 | 749 | // make the HTTP request 750 | 751 | var localVarResponse = await this.AsynchronousClient.{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}}Async<{{{returnType}}}{{^returnType}}Object{{/returnType}}>("{{{path}}}", localVarRequestOptions, this.Configuration, cancellationToken).ConfigureAwait(false); 752 | 753 | if (this.ExceptionFactory != null) 754 | { 755 | Exception _exception = this.ExceptionFactory("{{vendorExtensions.x-csharp-action}}", localVarResponse); 756 | if (_exception != null) throw _exception; 757 | } 758 | 759 | return localVarResponse; 760 | } 761 | 762 | {{/supportsAsync}} 763 | {{/operation}} 764 | } 765 | {{/operations}} 766 | } 767 | -------------------------------------------------------------------------------- /build/openapi-generator/templates/modelGeneric.mustache: -------------------------------------------------------------------------------- 1 | /// 2 | /// {{description}}{{^description}}{{classname}}{{/description}} 3 | /// 4 | {{#vendorExtensions.x-cls-compliant}} 5 | [CLSCompliant({{{vendorExtensions.x-cls-compliant}}})] 6 | {{/vendorExtensions.x-cls-compliant}} 7 | {{#vendorExtensions.x-com-visible}} 8 | [ComVisible({{{vendorExtensions.x-com-visible}}})] 9 | {{/vendorExtensions.x-com-visible}} 10 | [DataContract(Name = "{{{name}}}")] 11 | {{^useUnityWebRequest}} 12 | {{#discriminator}} 13 | [JsonConverter(typeof(JsonSubtypes), "{{{discriminatorName}}}")] 14 | {{#mappedModels}} 15 | [JsonSubtypes.KnownSubType(typeof({{{modelName}}}), "{{^vendorExtensions.x-discriminator-value}}{{{mappingName}}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{.}}}{{/vendorExtensions.x-discriminator-value}}")] 16 | {{/mappedModels}} 17 | {{/discriminator}} 18 | {{/useUnityWebRequest}} 19 | {{>visibility}} partial class {{classname}}{{#lambda.firstDot}}{{#parent}} : .{{/parent}}{{#validatable}} : .{{/validatable}}{{#equatable}} : .{{/equatable}}{{/lambda.firstDot}}{{#lambda.joinWithComma}}{{#parent}}{{{.}}} {{/parent}}{{#equatable}}IEquatable<{{classname}}> {{/equatable}}{{#validatable}}IValidatableObject {{/validatable}}{{/lambda.joinWithComma}} 20 | { 21 | {{#vars}} 22 | {{#items.isEnum}} 23 | {{#items}} 24 | {{^complexType}} 25 | {{>modelInnerEnum}} 26 | {{/complexType}} 27 | {{/items}} 28 | {{/items.isEnum}} 29 | {{#isEnum}} 30 | {{^complexType}} 31 | {{>modelInnerEnum}} 32 | {{/complexType}} 33 | {{/isEnum}} 34 | {{#isEnum}} 35 | 36 | /// 37 | /// {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} 38 | /// 39 | {{#description}} 40 | /// {{.}} 41 | {{/description}} 42 | {{#example}} 43 | /// {{.}} 44 | {{/example}} 45 | {{^conditionalSerialization}} 46 | [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] 47 | {{#deprecated}} 48 | [Obsolete] 49 | {{/deprecated}} 50 | public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} { get; set; } 51 | {{#isReadOnly}} 52 | 53 | /// 54 | /// Returns false as {{name}} should not be serialized given that it's read-only. 55 | /// 56 | /// false (boolean) 57 | public bool ShouldSerialize{{name}}() 58 | { 59 | return false; 60 | } 61 | {{/isReadOnly}} 62 | {{/conditionalSerialization}} 63 | {{#conditionalSerialization}} 64 | {{#isReadOnly}} 65 | [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] 66 | {{#deprecated}} 67 | [Obsolete] 68 | {{/deprecated}} 69 | public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} { get; set; } 70 | 71 | 72 | /// 73 | /// Returns false as {{name}} should not be serialized given that it's read-only. 74 | /// 75 | /// false (boolean) 76 | public bool ShouldSerialize{{name}}() 77 | { 78 | return false; 79 | } 80 | {{/isReadOnly}} 81 | 82 | {{^isReadOnly}} 83 | [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] 84 | {{#deprecated}} 85 | [Obsolete] 86 | {{/deprecated}} 87 | public {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} {{name}} 88 | { 89 | get{ return _{{name}};} 90 | set 91 | { 92 | _{{name}} = value; 93 | _flag{{name}} = true; 94 | } 95 | } 96 | private {{{complexType}}}{{^complexType}}{{{datatypeWithEnum}}}{{/complexType}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}} _{{name}}; 97 | private bool _flag{{name}}; 98 | 99 | /// 100 | /// Returns false as {{name}} should not be serialized given that it's read-only. 101 | /// 102 | /// false (boolean) 103 | public bool ShouldSerialize{{name}}() 104 | { 105 | return _flag{{name}}; 106 | } 107 | {{/isReadOnly}} 108 | {{/conditionalSerialization}} 109 | {{/isEnum}} 110 | {{/vars}} 111 | {{#hasRequired}} 112 | {{^hasOnlyReadOnly}} 113 | /// 114 | /// Initializes a new instance of the class. 115 | /// 116 | [JsonConstructorAttribute] 117 | {{^isAdditionalPropertiesTrue}} 118 | protected {{classname}}() { } 119 | {{/isAdditionalPropertiesTrue}} 120 | {{#isAdditionalPropertiesTrue}} 121 | protected {{classname}}() 122 | { 123 | this.AdditionalProperties = new Dictionary(); 124 | } 125 | {{/isAdditionalPropertiesTrue}} 126 | {{/hasOnlyReadOnly}} 127 | {{/hasRequired}} 128 | /// 129 | /// Initializes a new instance of the class. 130 | /// 131 | {{#readWriteVars}} 132 | /// {{description}}{{^description}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}{{/description}}{{#required}} (required){{/required}}{{#defaultValue}} (default to {{.}}){{/defaultValue}}. 133 | {{/readWriteVars}} 134 | {{#hasOnlyReadOnly}} 135 | [JsonConstructorAttribute] 136 | {{/hasOnlyReadOnly}} 137 | public {{classname}}({{#vars}}{{^isReadOnly}}{{{datatypeWithEnum}}}{{#isEnum}}{{^isContainer}}{{^required}}{{^conditionalSerialization}}?{{/conditionalSerialization}}{{/required}}{{/isContainer}}{{/isEnum}}{{#conditionalSerialization}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/conditionalSerialization}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{#defaultValue}}{{^isDateTime}}{{#isString}}{{^isEnum}}@{{/isEnum}}{{/isString}}{{{defaultValue}}}{{/isDateTime}}{{#isDateTime}}default({{{datatypeWithEnum}}}){{/isDateTime}}{{/defaultValue}}{{^defaultValue}}default({{{datatypeWithEnum}}}{{#isEnum}}{{^isContainer}}{{^required}}{{^conditionalSerialization}}?{{/conditionalSerialization}}{{/required}}{{/isContainer}}{{/isEnum}}{{#conditionalSerialization}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/conditionalSerialization}}){{/defaultValue}}{{^-last}}, {{/-last}}{{/isReadOnly}}{{/vars}}){{#parent}} : base({{#parentVars}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}{{^-last}}, {{/-last}}{{/parentVars}}){{/parent}} 138 | { 139 | {{#vars}} 140 | {{^isInherited}} 141 | {{^isReadOnly}} 142 | {{#required}} 143 | {{^conditionalSerialization}} 144 | {{^vendorExtensions.x-csharp-value-type}} 145 | // to ensure "{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}" is required (not null) 146 | if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} == null) 147 | { 148 | throw new ArgumentNullException("{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} is a required property for {{classname}} and cannot be null"); 149 | } 150 | this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; 151 | {{/vendorExtensions.x-csharp-value-type}} 152 | {{#vendorExtensions.x-csharp-value-type}} 153 | this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; 154 | {{/vendorExtensions.x-csharp-value-type}} 155 | {{/conditionalSerialization}} 156 | {{#conditionalSerialization}} 157 | {{^vendorExtensions.x-csharp-value-type}} 158 | // to ensure "{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}" is required (not null) 159 | if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} == null) 160 | { 161 | throw new ArgumentNullException("{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} is a required property for {{classname}} and cannot be null"); 162 | } 163 | this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; 164 | {{/vendorExtensions.x-csharp-value-type}} 165 | {{#vendorExtensions.x-csharp-value-type}} 166 | this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; 167 | {{/vendorExtensions.x-csharp-value-type}} 168 | {{/conditionalSerialization}} 169 | {{/required}} 170 | {{/isReadOnly}} 171 | {{/isInherited}} 172 | {{/vars}} 173 | {{#vars}} 174 | {{^isInherited}} 175 | {{^isReadOnly}} 176 | {{^required}} 177 | {{#defaultValue}} 178 | {{^conditionalSerialization}} 179 | {{^vendorExtensions.x-csharp-value-type}} 180 | // use default value if no "{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}" provided 181 | this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} ?? {{#isString}}@{{/isString}}{{{defaultValue}}}; 182 | {{/vendorExtensions.x-csharp-value-type}} 183 | {{#vendorExtensions.x-csharp-value-type}} 184 | this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; 185 | {{/vendorExtensions.x-csharp-value-type}} 186 | {{/conditionalSerialization}} 187 | {{/defaultValue}} 188 | {{^defaultValue}} 189 | {{^conditionalSerialization}} 190 | this.{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; 191 | {{/conditionalSerialization}} 192 | {{#conditionalSerialization}} 193 | this._{{name}} = {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}; 194 | if (this.{{name}} != null) 195 | { 196 | this._flag{{name}} = true; 197 | } 198 | {{/conditionalSerialization}} 199 | {{/defaultValue}} 200 | {{/required}} 201 | {{/isReadOnly}} 202 | {{/isInherited}} 203 | {{/vars}} 204 | {{#isAdditionalPropertiesTrue}} 205 | this.AdditionalProperties = new Dictionary(); 206 | {{/isAdditionalPropertiesTrue}} 207 | } 208 | 209 | {{#vars}} 210 | {{^isInherited}} 211 | {{^isEnum}} 212 | /// 213 | /// {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} 214 | /// {{#description}} 215 | /// {{.}}{{/description}} 216 | {{#example}} 217 | /// {{.}} 218 | {{/example}} 219 | {{^conditionalSerialization}} 220 | [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] 221 | {{#isDate}} 222 | [JsonConverter(typeof(OpenAPIDateConverter))] 223 | {{/isDate}} 224 | {{#deprecated}} 225 | [Obsolete] 226 | {{/deprecated}} 227 | public {{{dataType}}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}} {{name}} { get; {{#isReadOnly}}private {{/isReadOnly}}set; } 228 | 229 | {{#isReadOnly}} 230 | /// 231 | /// Returns false as {{name}} should not be serialized given that it's read-only. 232 | /// 233 | /// false (boolean) 234 | public bool ShouldSerialize{{name}}() 235 | { 236 | return false; 237 | } 238 | {{/isReadOnly}} 239 | {{/conditionalSerialization}} 240 | {{#conditionalSerialization}} 241 | {{#isReadOnly}} 242 | [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] 243 | {{#isDate}} 244 | [JsonConverter(typeof(OpenAPIDateConverter))] 245 | {{/isDate}} 246 | {{#deprecated}} 247 | [Obsolete] 248 | {{/deprecated}} 249 | public {{{dataType}}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}} {{name}} { get; private set; } 250 | 251 | /// 252 | /// Returns false as {{name}} should not be serialized given that it's read-only. 253 | /// 254 | /// false (boolean) 255 | public bool ShouldSerialize{{name}}() 256 | { 257 | return false; 258 | } 259 | {{/isReadOnly}} 260 | {{^isReadOnly}} 261 | {{#isDate}} 262 | [JsonConverter(typeof(OpenAPIDateConverter))] 263 | {{/isDate}} 264 | [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = {{#vendorExtensions.x-emit-default-value}}true{{/vendorExtensions.x-emit-default-value}}{{^vendorExtensions.x-emit-default-value}}{{#required}}true{{/required}}{{^required}}{{#isBoolean}}true{{/isBoolean}}{{^isBoolean}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/isBoolean}}{{/required}}{{/vendorExtensions.x-emit-default-value}})] 265 | {{#deprecated}} 266 | [Obsolete] 267 | {{/deprecated}} 268 | public {{{dataType}}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}} {{name}} 269 | { 270 | get{ return _{{name}};} 271 | set 272 | { 273 | _{{name}} = value; 274 | _flag{{name}} = true; 275 | } 276 | } 277 | private {{{dataType}}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}} _{{name}}; 278 | private bool _flag{{name}}; 279 | 280 | /// 281 | /// Returns false as {{name}} should not be serialized given that it's read-only. 282 | /// 283 | /// false (boolean) 284 | public bool ShouldSerialize{{name}}() 285 | { 286 | return _flag{{name}}; 287 | } 288 | {{/isReadOnly}} 289 | {{/conditionalSerialization}} 290 | {{/isEnum}} 291 | {{/isInherited}} 292 | {{/vars}} 293 | {{#isAdditionalPropertiesTrue}} 294 | /// 295 | /// Gets or Sets additional properties 296 | /// 297 | [JsonExtensionData] 298 | public IDictionary AdditionalProperties { get; set; } 299 | 300 | {{/isAdditionalPropertiesTrue}} 301 | /// 302 | /// Returns the string presentation of the object 303 | /// 304 | /// String presentation of the object 305 | public override string ToString() 306 | { 307 | StringBuilder sb = new StringBuilder(); 308 | sb.Append("class {{classname}} {\n"); 309 | {{#parent}} 310 | sb.Append(" ").Append(base.ToString().Replace("\n", "\n ")).Append("\n"); 311 | {{/parent}} 312 | {{#vars}} 313 | sb.Append(" {{name}}: ").Append({{name}}).Append("\n"); 314 | {{/vars}} 315 | {{#isAdditionalPropertiesTrue}} 316 | sb.Append(" AdditionalProperties: ").Append(AdditionalProperties).Append("\n"); 317 | {{/isAdditionalPropertiesTrue}} 318 | sb.Append("}\n"); 319 | return sb.ToString(); 320 | } 321 | 322 | /// 323 | /// Returns the JSON string presentation of the object 324 | /// 325 | /// JSON string presentation of the object 326 | public {{#parent}}{{^isArray}}{{^isMap}}override {{/isMap}}{{/isArray}}{{/parent}}{{^parent}}virtual {{/parent}}string ToJson() 327 | { 328 | return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); 329 | } 330 | {{#equatable}} 331 | 332 | /// 333 | /// Returns true if objects are equal 334 | /// 335 | /// Object to be compared 336 | /// Boolean 337 | public override bool Equals(object input) 338 | { 339 | {{#useCompareNetObjects}} 340 | return OpenAPIClientUtils.compareLogic.Compare(this, input as {{classname}}).AreEqual; 341 | {{/useCompareNetObjects}} 342 | {{^useCompareNetObjects}} 343 | return this.Equals(input as {{classname}}); 344 | {{/useCompareNetObjects}} 345 | } 346 | 347 | /// 348 | /// Returns true if {{classname}} instances are equal 349 | /// 350 | /// Instance of {{classname}} to be compared 351 | /// Boolean 352 | public bool Equals({{classname}} input) 353 | { 354 | {{#useCompareNetObjects}} 355 | return OpenAPIClientUtils.compareLogic.Compare(this, input).AreEqual; 356 | {{/useCompareNetObjects}} 357 | {{^useCompareNetObjects}} 358 | if (input == null) 359 | { 360 | return false; 361 | } 362 | return {{#vars}}{{#parent}}base.Equals(input) && {{/parent}}{{^isContainer}} 363 | ( 364 | this.{{name}} == input.{{name}} || 365 | {{^vendorExtensions.x-is-value-type}} 366 | (this.{{name}} != null && 367 | this.{{name}}.Equals(input.{{name}})) 368 | {{/vendorExtensions.x-is-value-type}} 369 | {{#vendorExtensions.x-is-value-type}} 370 | this.{{name}}.Equals(input.{{name}}) 371 | {{/vendorExtensions.x-is-value-type}} 372 | ){{^-last}} && {{/-last}}{{/isContainer}}{{#isContainer}} 373 | ( 374 | this.{{name}} == input.{{name}} || 375 | {{^vendorExtensions.x-is-value-type}}this.{{name}} != null && 376 | input.{{name}} != null && 377 | {{/vendorExtensions.x-is-value-type}}this.{{name}}.SequenceEqual(input.{{name}}) 378 | ){{^-last}} && {{/-last}}{{/isContainer}}{{/vars}}{{^vars}}{{#parent}}base.Equals(input){{/parent}}{{^parent}}false{{/parent}}{{/vars}}{{^isAdditionalPropertiesTrue}};{{/isAdditionalPropertiesTrue}} 379 | {{#isAdditionalPropertiesTrue}} 380 | && (this.AdditionalProperties.Count == input.AdditionalProperties.Count && !this.AdditionalProperties.Except(input.AdditionalProperties).Any()); 381 | {{/isAdditionalPropertiesTrue}} 382 | {{/useCompareNetObjects}} 383 | } 384 | 385 | /// 386 | /// Gets the hash code 387 | /// 388 | /// Hash code 389 | public override int GetHashCode() 390 | { 391 | unchecked // Overflow is fine, just wrap 392 | { 393 | {{#parent}} 394 | int hashCode = base.GetHashCode(); 395 | {{/parent}} 396 | {{^parent}} 397 | int hashCode = 41; 398 | {{/parent}} 399 | {{#vars}} 400 | {{^vendorExtensions.x-is-value-type}} 401 | if (this.{{name}} != null) 402 | { 403 | hashCode = (hashCode * 59) + this.{{name}}.GetHashCode(); 404 | } 405 | {{/vendorExtensions.x-is-value-type}} 406 | {{#vendorExtensions.x-is-value-type}} 407 | hashCode = (hashCode * 59) + this.{{name}}.GetHashCode(); 408 | {{/vendorExtensions.x-is-value-type}} 409 | {{/vars}} 410 | {{#isAdditionalPropertiesTrue}} 411 | if (this.AdditionalProperties != null) 412 | { 413 | hashCode = (hashCode * 59) + this.AdditionalProperties.GetHashCode(); 414 | } 415 | {{/isAdditionalPropertiesTrue}} 416 | return hashCode; 417 | } 418 | } 419 | {{/equatable}} 420 | 421 | {{#validatable}} 422 | {{>validatable}} 423 | {{/validatable}} 424 | } 425 | -------------------------------------------------------------------------------- /build/services/BuildPaths.cs: -------------------------------------------------------------------------------- 1 | using Nuke.Common; 2 | using Nuke.Common.IO; 3 | 4 | public static class BuildPaths 5 | { 6 | public static AbsolutePath Readme => NukeBuild.RootDirectory / "README.md"; 7 | 8 | public static class OpenApiJson 9 | { 10 | public static readonly AbsolutePath Downloaded = NukeBuild.RootDirectory / "keycloak.openapi.json"; 11 | public static readonly AbsolutePath Formatted = NukeBuild.RootDirectory / "keycloak.openapi.formatted.json"; 12 | public static readonly AbsolutePath Fixed = NukeBuild.RootDirectory / "keycloak.openapi.fixed.json"; 13 | public static readonly AbsolutePath Escaped = NukeBuild.RootDirectory / "keycloak.openapi.escaped.json"; 14 | public static readonly AbsolutePath Humanized = NukeBuild.RootDirectory / "keycloak.openapi.humanized.json"; 15 | } 16 | 17 | public static class Generator 18 | { 19 | public static readonly AbsolutePath Path = NukeBuild.RootDirectory / "build" / "openapi-generator"; 20 | public static readonly AbsolutePath Templates = Path / "templates"; 21 | public static readonly AbsolutePath ToolConfiguration = Path / "openapitools.json"; 22 | public static readonly AbsolutePath GeneratorConfiguration = Path / "openapi-generator.config.json"; 23 | } 24 | 25 | public static class Client 26 | { 27 | public static readonly AbsolutePath Path = NukeBuild.RootDirectory / "FS.Keycloak.RestApiClient"; 28 | public static readonly AbsolutePath ProjectFile = Path / "src" / "FS.Keycloak.RestApiClient" / "FS.Keycloak.RestApiClient.csproj"; 29 | public static readonly AbsolutePath Readme = Path / "README.md"; 30 | } 31 | } -------------------------------------------------------------------------------- /build/services/ClientGenerator.cs: -------------------------------------------------------------------------------- 1 | using Nuke.Common; 2 | using Nuke.Common.Tooling; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | public class ClientGenerator 9 | { 10 | private static readonly Tool _openapiGeneratorCmd = ToolResolver.GetPathTool(NukeBuild.RootDirectory / $"build/openapi-generator/node_modules/.bin/openapi-generator-cli{(EnvironmentInfo.IsWin ? ".cmd" : "")}"); 11 | 12 | public static void GenerateClient(string openApiSpec, string outputFolder) 13 | { 14 | List arguments = 15 | [ 16 | $"generate", 17 | $"--openapitools {BuildPaths.Generator.ToolConfiguration}", 18 | $"--config {BuildPaths.Generator.GeneratorConfiguration}", 19 | $"--generator-name csharp", 20 | $"--template-dir {BuildPaths.Generator.Templates}", 21 | $"--type-mappings date-span='TimeSpan'", 22 | $"--global-property apiTests=false", 23 | $"--output {outputFolder}", 24 | $"--input-spec {openApiSpec}" 25 | ]; 26 | 27 | var environmentVariables = GetEnvironmentVariables(); 28 | environmentVariables.Add("JAVA_OPTS", "-Dlog.level=error"); 29 | _openapiGeneratorCmd(string.Join(" ", arguments), environmentVariables: environmentVariables); 30 | } 31 | 32 | private static Dictionary GetEnvironmentVariables() 33 | => Environment.GetEnvironmentVariables() 34 | .Cast() 35 | .ToDictionary(x => (string)x.Key, x => (string?)x.Value); 36 | } -------------------------------------------------------------------------------- /build/services/Npm.cs: -------------------------------------------------------------------------------- 1 | using Nuke.Common.Tooling; 2 | using Nuke.Common.Tools.Npm; 3 | 4 | public static class Npm 5 | { 6 | public static void Restore(string path) 7 | => NpmTasks.NpmCi(settings => settings 8 | .SetProcessWorkingDirectory(path) 9 | .AddProcessAdditionalArguments( 10 | "--prefer-offline", 11 | "--no-audit" 12 | )); 13 | } -------------------------------------------------------------------------------- /build/services/OpenApiSpec.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Any; 2 | using Microsoft.OpenApi.Models; 3 | using Microsoft.OpenApi.Readers; 4 | using Microsoft.OpenApi.Writers; 5 | using Nuke.Common; 6 | using Nuke.Common.IO; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text.RegularExpressions; 12 | 13 | public static class OpenApiSpec 14 | { 15 | public static void Download(string sourceOpenApiJson, string localOpenApiJson) 16 | { 17 | if (sourceOpenApiJson == localOpenApiJson) 18 | return; 19 | 20 | if (sourceOpenApiJson.StartsWith("http")) 21 | HttpTasks.HttpDownloadFile(sourceOpenApiJson, localOpenApiJson); 22 | 23 | if (File.Exists(sourceOpenApiJson)) 24 | File.Copy(sourceOpenApiJson, localOpenApiJson, true); 25 | 26 | Assert.FileExists(localOpenApiJson); 27 | } 28 | 29 | public static void Reformat(string source, string destination) 30 | { 31 | var document = ReadOpenApiJson(source); 32 | WriteOpenApiJson(document, destination); 33 | } 34 | 35 | public static void ApplyFixes(string source, string destination) 36 | { 37 | var document = ReadOpenApiJson(source); 38 | 39 | // Fix 'ClientPolicyConditionRepresentation' 40 | var clientPolicyConditionRepresentation = document.Components.Schemas.Get("ClientPolicyConditionRepresentation")?.Properties.Get("configuration"); 41 | if (clientPolicyConditionRepresentation != null) 42 | clientPolicyConditionRepresentation.Type = "string"; 43 | 44 | // Fix 'ClientPolicyExecutorRepresentation' 45 | var clientPolicyExecutorRepresentation = document.Components.Schemas.Get("ClientPolicyExecutorRepresentation")?.Properties.Get("configuration"); 46 | if (clientPolicyExecutorRepresentation != null) 47 | clientPolicyExecutorRepresentation.Type = "string"; 48 | 49 | // Fix realm import request body type 50 | var adminRealmsRequestBody = document.Paths.Get("/admin/realms")?.Operations.Get(OperationType.Post)?.RequestBody.Content.Get("application/json"); 51 | if (adminRealmsRequestBody != null) 52 | { 53 | adminRealmsRequestBody.Schema.Format = null; 54 | adminRealmsRequestBody.Schema.Type = null; 55 | adminRealmsRequestBody.Schema.Reference = document.Components.Schemas.Get("RealmRepresentation")?.Reference 56 | ?? throw new InvalidOperationException("Schema for 'RealmRepresentation' not found"); 57 | } 58 | 59 | // Remove deprecated members producing name conflicts 60 | document.Components.Schemas.Get("AccessToken")?.Properties.Remove("authTime"); 61 | document.Components.Schemas.Get("IDToken")?.Properties.Remove("authTime"); 62 | 63 | // Remove deprecated members producing error on realm import, see https://github.com/fschick/Keycloak.RestApiClient/issues/4#issuecomment-2071597287 64 | document.Components.Schemas.Get("RealmRepresentation")?.Properties.Remove("userCacheEnabled"); 65 | document.Components.Schemas.Get("RealmRepresentation")?.Properties.Remove("realmCacheEnabled"); 66 | document.Components.Schemas.Get("RealmRepresentation")?.Properties.Remove("oAuth2DeviceCodeLifespan"); 67 | document.Components.Schemas.Get("RealmRepresentation")?.Properties.Remove("oAuth2DevicePollingInterval"); 68 | 69 | // Fix wrong content type, https://github.com/fschick/Keycloak.RestApiClient/issues/11 70 | var adminRealmsOrganizationMembersRequestBody = document.Paths.Get("/admin/realms/{realm}/organizations/{id}/members")?.Operations.Get(OperationType.Post)?.RequestBody; 71 | if (adminRealmsOrganizationMembersRequestBody != null) 72 | { 73 | var jsonSchema = adminRealmsOrganizationMembersRequestBody.Content.First(medaType => medaType.Key == "application/json"); 74 | adminRealmsOrganizationMembersRequestBody.Content.Remove(jsonSchema.Key); 75 | adminRealmsOrganizationMembersRequestBody.Content.Add("text/plain", jsonSchema.Value); 76 | } 77 | 78 | // Fix duplicate parameter names, https://github.com/fschick/Keycloak.RestApiClient/issues/15 79 | var adminRealmOrganizationMember = document.Paths.Get("/admin/realms/{realm}/organizations/{id}/members/{id}"); 80 | if (adminRealmOrganizationMember != null) 81 | { 82 | foreach (var operation in adminRealmOrganizationMember.Operations.Values) 83 | { 84 | var originIdParam = operation.Parameters.First(parameter => parameter.Name == "id"); 85 | var organizationIdParam = new OpenApiParameter(originIdParam) { Name = "organizationId" }; 86 | var userIdParam = new OpenApiParameter(originIdParam) { Name = "userId" }; 87 | 88 | operation.Parameters.Remove(originIdParam); 89 | operation.Parameters.Add(organizationIdParam); 90 | operation.Parameters.Add(userIdParam); 91 | } 92 | 93 | document.Paths.Remove("/admin/realms/{realm}/organizations/{id}/members/{id}"); 94 | document.Paths.Add("/admin/realms/{realm}/organizations/{organizationId}/members/{userId}", adminRealmOrganizationMember); 95 | } 96 | 97 | // Fix duplicate parameter names, https://github.com/fschick/Keycloak.RestApiClient/issues/15 98 | var adminRealmOrganizationMemberOrganizations = document.Paths.Get("/admin/realms/{realm}/organizations/{id}/members/{id}/organizations"); 99 | if (adminRealmOrganizationMemberOrganizations != null) 100 | { 101 | foreach (var operation in adminRealmOrganizationMemberOrganizations.Operations.Values) 102 | { 103 | var originIdParam = operation.Parameters.First(parameter => parameter.Name == "id"); 104 | var organizationIdParam = new OpenApiParameter(originIdParam) { Name = "organizationId" }; 105 | var userIdParam = new OpenApiParameter(originIdParam) { Name = "userId" }; 106 | 107 | operation.Parameters.Remove(originIdParam); 108 | operation.Parameters.Add(organizationIdParam); 109 | operation.Parameters.Add(userIdParam); 110 | } 111 | 112 | document.Paths.Remove("/admin/realms/{realm}/organizations/{id}/members/{id}/organizations"); 113 | document.Paths.Add("/admin/realms/{realm}/organizations/{organizationId}/members/{userId}/organizations", adminRealmOrganizationMemberOrganizations); 114 | } 115 | 116 | WriteOpenApiJson(document, destination); 117 | } 118 | 119 | public static void EscapeForXmlDocumentation(string source, string destination) 120 | { 121 | var document = ReadOpenApiJson(source); 122 | var schemas = document.Components.Schemas.Values.ToList(); 123 | var properties = schemas.SelectMany(schema => schema.Properties.Values).ToList(); 124 | var paths = document.Paths.Values.ToList(); 125 | var operations = paths.SelectMany(path => path.Operations.Values).ToList(); 126 | var parameters = operations.SelectMany(operation => operation.Parameters).ToList(); 127 | var responses = operations.SelectMany(operation => operation.Responses.Values).ToList(); 128 | 129 | document.Info.Description = document.Info.Description.EscapeForXmlDocumentation(); 130 | 131 | foreach (var schema in schemas) 132 | schema.Description = schema.Description.EscapeForXmlDocumentation(); 133 | 134 | foreach (var property in properties) 135 | property.Description = property.Description.EscapeForXmlDocumentation(); 136 | 137 | foreach (var path in paths) 138 | path.Description = path.Description.EscapeForXmlDocumentation(); 139 | 140 | foreach (var operation in operations) 141 | operation.Description = operation.Description.EscapeForXmlDocumentation(); 142 | 143 | foreach (var path in parameters) 144 | path.Description = path.Description.EscapeForXmlDocumentation(); 145 | 146 | foreach (var path in responses) 147 | path.Description = path.Description.EscapeForXmlDocumentation(); 148 | 149 | WriteOpenApiJson(document, destination); 150 | } 151 | 152 | public static void HumanizeActionNames(string source, string destination) 153 | { 154 | var document = ReadOpenApiJson(source); 155 | 156 | foreach (var (pathName, path) in document.Paths) 157 | { 158 | foreach (var (operationType, operation) in path.Operations) 159 | { 160 | var humanizedPath = HumanizePath(pathName, operationType); 161 | operation.Extensions.Add("x-csharp-action", new OpenApiString(humanizedPath)); 162 | } 163 | } 164 | 165 | WriteOpenApiJson(document, destination); 166 | } 167 | 168 | private static OpenApiDocument ReadOpenApiJson(string source) 169 | { 170 | using var streamReader = new StreamReader(source); 171 | var reader = new OpenApiStreamReader(); 172 | return reader.Read(streamReader.BaseStream, out _); 173 | } 174 | 175 | private static void WriteOpenApiJson(OpenApiDocument document, string destination) 176 | { 177 | using var streamWriter = new StreamWriter(destination); 178 | var writer = new OpenApiJsonWriter(streamWriter); 179 | document.SerializeAsV3(writer); 180 | } 181 | 182 | private static string? HumanizePath(string? path, OperationType operationType) 183 | { 184 | if (string.IsNullOrWhiteSpace(path)) 185 | return path; 186 | 187 | var parts = Regex 188 | .Replace(path, "^/admin/realms", string.Empty) 189 | .Split('/') 190 | .Where(part => !string.IsNullOrEmpty(part) && part != "{realm}") 191 | .ToList(); 192 | 193 | var pathParts = parts 194 | .Where(part => !part.StartsWith('{')) 195 | .Select(HumanizePathPart) 196 | .ToList(); 197 | 198 | var parameterParts = parts 199 | .Where(part => part.StartsWith('{') && part.EndsWith('}')) 200 | .Select(part => part.Trim('{', '}')) 201 | .Select(HumanizePathPart) 202 | .ToList(); 203 | 204 | var operation = operationType.ToString().UppercaseFirstChar(); 205 | var camelCasedPath = $"{operation}{pathParts.Join()}"; 206 | 207 | if (parameterParts.Count > 0) 208 | camelCasedPath += $"By{parameterParts.Join("And")}"; 209 | 210 | return camelCasedPath; 211 | } 212 | 213 | private static string HumanizePathPart(string pathPart) 214 | => pathPart 215 | .Split('-', '_') 216 | .Select(UppercaseFirstChar) 217 | .Join(); 218 | 219 | private static string? EscapeForXmlDocumentation(this string? text) 220 | => text?.Replace('<', '{').Replace('>', '}'); 221 | 222 | private static TResult? Get(this IDictionary dictionary, TKey key) where TKey : notnull 223 | => dictionary.TryGetValue(key, out var value) 224 | ? value 225 | : default; 226 | 227 | private static string UppercaseFirstChar(this string input) 228 | { 229 | if (string.IsNullOrEmpty(input)) 230 | return input; 231 | 232 | return string.Concat(input[0].ToString().ToUpper(), input.AsSpan(1)); 233 | } 234 | 235 | private static string Join(this IEnumerable input, string separator = "") 236 | => string.Join(separator, input); 237 | } -------------------------------------------------------------------------------- /build/services/Project.cs: -------------------------------------------------------------------------------- 1 | using Nuke.Common; 2 | using Nuke.Common.IO; 3 | using Nuke.Common.Tooling; 4 | using Nuke.Common.Tools.DotNet; 5 | using System.IO; 6 | using System.Text.RegularExpressions; 7 | 8 | public static class Project 9 | { 10 | public static void AdjustImports(string projectFile) 11 | { 12 | const string projectImports = """ 13 | 14 | 15 | 16 | 17 | 18 | 1701;1702;IDE0079;CS0472;CS0612 19 | 20 | 21 | 22 | 23 | 24 | 25 | """; 26 | 27 | var project = File.ReadAllText(projectFile); 28 | 29 | project = Regex.Replace(project, ".*<\\/PropertyGroup>", string.Empty, RegexOptions.Singleline); 30 | project = Regex.Replace(project, "((?!PackageReference).)*?<\\/ItemGroup>", string.Empty, RegexOptions.Singleline); 31 | project = Regex.Replace(project, "", $"\n{projectImports}"); 32 | 33 | File.WriteAllText(projectFile, project); 34 | } 35 | 36 | public static void CopyReadme(string source, string destination) 37 | => File.Copy(source, destination, true); 38 | 39 | public static void Build(string projectFile, string version) 40 | { 41 | var fileVersion = GetFileVersion(version); 42 | 43 | DotNetTasks.DotNetBuild(settings => settings 44 | .SetProjectFile(projectFile) 45 | .SetConfiguration(Configuration.Debug) 46 | ); 47 | 48 | DotNetTasks.DotNetBuild(settings => settings 49 | .SetProjectFile(projectFile) 50 | .SetConfiguration(Configuration.Release) 51 | .SetWarningsAsErrors() 52 | .SetVersion(version) 53 | .SetFileVersion(fileVersion) 54 | ); 55 | } 56 | 57 | public static void PackNuget(string projectFile, string version, string? publishFolder = null) 58 | { 59 | var fileVersion = GetFileVersion(version); 60 | 61 | DotNetTasks.DotNetPack(settings => settings 62 | .SetProject(projectFile) 63 | .SetConfiguration(Configuration.Release) 64 | .SetVersion(version) 65 | .SetFileVersion(fileVersion) 66 | .SetOutputDirectory(publishFolder) 67 | ); 68 | } 69 | 70 | public static void PushNuget(AbsolutePath publishFolder, string? nugetApiKey, string? nugetServerUrl = null) 71 | { 72 | if (!Path.IsPathRooted(publishFolder)) 73 | publishFolder = NukeBuild.RootDirectory + publishFolder; 74 | 75 | nugetServerUrl ??= "https://api.nuget.org/v3/index.json"; 76 | 77 | var nuGetPackages = publishFolder.GlobFiles("*.nupkg"); 78 | 79 | DotNetTasks.DotNetNuGetPush(settings => settings 80 | .SetSource(nugetServerUrl) 81 | .SetApiKey(nugetApiKey) 82 | .CombineWith(nuGetPackages, (settings, package) => settings.SetTargetPath(package)) 83 | ); 84 | } 85 | 86 | private static string GetFileVersion(string version) 87 | => Regex.Replace(version, @"(\d+(?:\.\d+)*)(.*)", "$1"); 88 | } -------------------------------------------------------------------------------- /build/targets/net_standard.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 1701;1702;IDE0079 9 | 10 | 11 | -------------------------------------------------------------------------------- /build/targets/nuget.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([MSBuild]::ValueOrDefault('$(AssemblyName)', '').Replace('FS.', 'Schick.')) 5 | $(Description) 6 | Schick.Keycloak.RestApiClient.png 7 | README.md 8 | MIT 9 | $(Tags) 10 | 11 | 12 | -------------------------------------------------------------------------------- /build/targets/version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Keycloak Admin REST API client 5 | $(Product) 6 | $(Product) 7 | Florian Schick 8 | Schick Software Entwicklung 9 | © Florian Schick, 2022 all rights reserved 10 | $(Company) 11 | $(Description) 12 | 13 | 14 | 0.0.0.0 15 | 0.0.0.0 16 | $(FileVersion) 17 | 18 | https://github.com/fschick/Keycloak.RestApiClient.git 19 | git 20 | 21 | 22 | --------------------------------------------------------------------------------