From 24366be0db4d6baf9da7a426803a645804c79922 Mon Sep 17 00:00:00 2001 From: devhl-labs Date: Thu, 27 Jan 2022 23:05:36 -0500 Subject: [PATCH] [csharp-netcore] Adding generic host library (#10627) * added generichost library * added templates * added an event, improved docs, added logging * adding event args file * fixed hard coded package name * added an AddTokens overload for a single token * changed api clients to singletons to support the event registration * build samples * log exceptions while executing api responded event * nrt bug fixes, dangling comma fix * resolving comments * removed debugging lines * refactored token provider * rate limit provider now default * updated readme, added ConfigureAwait(false) * DI fixes * removed a hard coded project name * fixed nrt bugs * improved NRT and .net 3.1 support * renamed projectName to apiName, added cli option * trying to avoid conflict * set GenerateAssemlbyInfo to true * created docs/scripts folder * moved ApiTestsBase.cs to not get overwritten * test fixes and improvements * fixed licenseId bug, updated readme * build samples * export docs * removed new language features * added support for .net standard 2.0 * added git_push.ps1 * fixed bug in git_push.sh due to the new directory, prompting user for commit message * moved documentation folders * fixed bug when apiKey in query * bug fix --- docs/generators/csharp-netcore.md | 3 +- .../codegen/CodegenConstants.java | 2 + .../languages/CSharpNetCoreClientCodegen.java | 173 ++++- .../resources/csharp-netcore/api_doc.mustache | 2 +- .../generichost/ApiException.mustache | 44 ++ .../generichost/ApiKeyToken.mustache | 59 ++ .../generichost/ApiResponseEventArgs.mustache | 47 ++ .../generichost/ApiResponse`1.mustache | 97 +++ .../generichost/ApiTestsBase.mustache | 47 ++ .../libraries/generichost/BasicToken.mustache | 44 ++ .../generichost/BearerToken.mustache | 39 + .../generichost/ClientUtils.mustache | 360 ++++++++++ .../DependencyInjectionTests.mustache | 158 ++++ .../generichost/HostConfiguration.mustache | 124 ++++ .../HttpSigningConfiguration.mustache | 676 ++++++++++++++++++ .../generichost/HttpSigningToken.mustache | 43 ++ .../libraries/generichost/IApi.mustache | 21 + .../libraries/generichost/OAuthToken.mustache | 39 + .../libraries/generichost/README.mustache | 214 ++++++ .../generichost/RateLimitProvider`1.mustache | 106 +++ .../libraries/generichost/TokenBase.mustache | 71 ++ .../generichost/TokenContainer`1.mustache | 37 + .../generichost/TokenProvider`1.mustache | 36 + .../libraries/generichost/api.mustache | 391 ++++++++++ .../libraries/generichost/api_test.mustache | 46 ++ .../generichost/git_push.ps1.mustache | 75 ++ .../generichost/git_push.sh.mustache | 49 ++ .../csharp-netcore/modelAnyOf.mustache | 2 + .../csharp-netcore/modelOneOf.mustache | 2 + .../csharp-netcore/model_doc.mustache | 2 +- .../csharp-netcore/netcore_project.mustache | 12 +- .../netcore_testproject.mustache | 3 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- .../Org.OpenAPITools/Org.OpenAPITools.csproj | 2 +- 40 files changed, 3011 insertions(+), 29 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiException.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiKeyToken.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponseEventArgs.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponse`1.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiTestsBase.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BasicToken.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BearerToken.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ClientUtils.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/DependencyInjectionTests.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HostConfiguration.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningConfiguration.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningToken.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/IApi.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/OAuthToken.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/README.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/RateLimitProvider`1.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenBase.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenContainer`1.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenProvider`1.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api_test.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.ps1.mustache create mode 100644 modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.sh.mustache diff --git a/docs/generators/csharp-netcore.md b/docs/generators/csharp-netcore.md index d693fdf9e441..17bc2cfa9745 100644 --- a/docs/generators/csharp-netcore.md +++ b/docs/generators/csharp-netcore.md @@ -19,12 +19,13 @@ These options may be applied as additional-properties (cli) or configOptions (pl | Option | Description | Values | Default | | ------ | ----------- | ------ | ------- | |allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| +|apiName|Must be a valid C# class name. Only used in Generic Host library. Default: Api| |Api| |caseInsensitiveResponseHeaders|Make API response's headers case-insensitive| |false| |conditionalSerialization|Serialize only those properties which are initialized by user, accepted values are true or false, default value is false.| |false| |disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|
**false**
The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
**true**
Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.
|true| |hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |true| |interfacePrefix|Prefix interfaces with a community standard or widely accepted prefix.| |I| -|library|HTTP library template (sub-template) to use|
**httpclient**
HttpClient (https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) (Experimental. May subject to breaking changes without further notice.)
**restsharp**
RestSharp (https://github.com/restsharp/RestSharp)
|restsharp| +|library|HTTP library template (sub-template) to use|
**generichost**
HttpClient with Generic Host dependency injection (https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host) (Experimental. Subject to breaking changes without notice.)
**httpclient**
HttpClient (https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) (Experimental. Subject to breaking changes without notice.)
**restsharp**
RestSharp (https://github.com/restsharp/RestSharp)
|restsharp| |licenseId|The identifier of the license| |null| |modelPropertyNaming|Naming convention for the property: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |PascalCase| |netCoreProjectFile|Use the new format (.NET Core) for .NET project files (.csproj).| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java index ffbd0c5e0a4b..27c47456bc0c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java @@ -35,6 +35,8 @@ public class CodegenConstants { public static final String SKIP_FORM_MODEL = "skipFormModel"; /* /end System Properties */ + public static final String API_NAME = "apiName"; + public static final String API_PACKAGE = "apiPackage"; public static final String API_PACKAGE_DESC = "package for generated api classes"; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java index e211deb48abe..67cfd2f1cfae 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpNetCoreClientCodegen.java @@ -39,7 +39,9 @@ import static org.openapitools.codegen.utils.StringUtils.underscore; @SuppressWarnings("Duplicates") -public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen { +public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen { + protected String apiName = "Api"; + // Defines the sdk option for targeted frameworks, which differs from targetFramework and targetFrameworkNuget protected static final String MCS_NET_VERSION_KEY = "x-mcs-sdk"; protected static final String SUPPORTS_UWP = "supportsUWP"; @@ -50,6 +52,7 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen { // HTTP libraries protected static final String RESTSHARP = "restsharp"; protected static final String HTTPCLIENT = "httpclient"; + protected static final String GENERICHOST = "generichost"; // Project Variable, determined from target framework. Not intended to be user-settable. protected static final String TARGET_FRAMEWORK_IDENTIFIER = "targetFrameworkIdentifier"; @@ -172,6 +175,10 @@ public CSharpNetCoreClientCodegen() { "C# package name (convention: Title.Case).", this.packageName); + addOption(CodegenConstants.API_NAME, + "Must be a valid C# class name. Only used in Generic Host library. Default: " + this.apiName, + this.apiName); + addOption(CodegenConstants.PACKAGE_VERSION, "C# package version.", this.packageVersion); @@ -269,8 +276,8 @@ public CSharpNetCoreClientCodegen() { this.optionalEmitDefaultValuesFlag); addSwitch(CodegenConstants.OPTIONAL_CONDITIONAL_SERIALIZATION, - CodegenConstants.OPTIONAL_CONDITIONAL_SERIALIZATION_DESC, - this.conditionalSerialization); + CodegenConstants.OPTIONAL_CONDITIONAL_SERIALIZATION_DESC, + this.conditionalSerialization); addSwitch(CodegenConstants.OPTIONAL_PROJECT_FILE, CodegenConstants.OPTIONAL_PROJECT_FILE_DESC, @@ -310,8 +317,10 @@ public CSharpNetCoreClientCodegen() { regexModifiers.put('s', "Singleline"); regexModifiers.put('x', "IgnorePatternWhitespace"); + supportedLibraries.put(GENERICHOST, "HttpClient with Generic Host dependency injection (https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host) " + + "(Experimental. Subject to breaking changes without notice.)"); supportedLibraries.put(HTTPCLIENT, "HttpClient (https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) " - + "(Experimental. May subject to breaking changes without further notice.)"); + + "(Experimental. Subject to breaking changes without notice.)"); supportedLibraries.put(RESTSHARP, "RestSharp (https://github.com/restsharp/RestSharp)"); CliOption libraryOption = new CliOption(CodegenConstants.LIBRARY, "HTTP library template (sub-template) to use"); @@ -324,7 +333,11 @@ public CSharpNetCoreClientCodegen() { @Override public String apiDocFileFolder() { - return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); + if (GENERICHOST.equals(getLibrary())){ + return (outputFolder + "/" + apiDocPath + File.separatorChar + "apis").replace('/', File.separatorChar); + }else{ + return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); + } } @Override @@ -454,7 +467,11 @@ public void setNonPublicApi(final boolean nonPublicApi) { @Override public String modelDocFileFolder() { - return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar); + if (GENERICHOST.equals(getLibrary())){ + return (outputFolder + "/" + modelDocPath + File.separator + "models").replace('/', File.separatorChar); + }else{ + return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar); + } } @Override @@ -559,6 +576,16 @@ public void processOpts() { setModelPropertyNaming((String) additionalProperties.get(CodegenConstants.MODEL_PROPERTY_NAMING)); } + if (additionalProperties.containsKey((CodegenConstants.LICENSE_ID))) { + setLicenseId((String) additionalProperties.get(CodegenConstants.LICENSE_ID)); + } + + if (additionalProperties.containsKey(CodegenConstants.API_NAME)) { + setApiName((String) additionalProperties.get(CodegenConstants.API_NAME)); + } else { + additionalProperties.put(CodegenConstants.API_NAME, apiName); + } + if (isEmpty(apiPackage)) { setApiPackage("Api"); } @@ -568,7 +595,10 @@ public void processOpts() { clientPackage = "Client"; - if (RESTSHARP.equals(getLibrary())) { + if (GENERICHOST.equals(getLibrary())){ + setLibrary(GENERICHOST); + additionalProperties.put("useGenericHost", true); + } else if (RESTSHARP.equals(getLibrary())) { additionalProperties.put("useRestSharp", true); needsCustomHttpMethod = true; } else if (HTTPCLIENT.equals(getLibrary())) { @@ -670,11 +700,59 @@ public void processOpts() { binRelativePath += "vendor"; additionalProperties.put("binRelativePath", binRelativePath); + // Only write out test related files if excludeTests is unset or explicitly set to false (see start of this method) + if (Boolean.FALSE.equals(excludeTests.get())) { + modelTestTemplateFiles.put("model_test.mustache", ".cs"); + apiTestTemplateFiles.put("api_test.mustache", ".cs"); + } + if(HTTPCLIENT.equals(getLibrary())) { supportingFiles.add(new SupportingFile("FileParameter.mustache", clientPackageDir, "FileParameter.cs")); typeMapping.put("file", "FileParameter"); + addRestSharpSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir); + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + } + else if (GENERICHOST.equals(getLibrary())){ + addGenericHostSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir); + additionalProperties.put("apiDocPath", apiDocPath + File.separatorChar + "apis"); + additionalProperties.put("modelDocPath", modelDocPath + File.separatorChar + "models"); } + else{ + addRestSharpSupportingFiles(clientPackageDir, packageFolder, excludeTests, testPackageFolder, testPackageName, modelPackageDir); + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + } + + addTestInstructions(); + } + + private void addTestInstructions(){ + if (GENERICHOST.equals(getLibrary())){ + additionalProperties.put("testInstructions", + "/* *********************************************************************************" + + "\n* Follow these manual steps to construct tests." + + "\n* This file will not be overwritten." + + "\n* *********************************************************************************" + + "\n* 1. Navigate to ApiTests.Base.cs and ensure any tokens are being created correctly." + + "\n* Take care not to commit credentials to any repository." + + "\n*" + + "\n* 2. Mocking is coordinated by ApiTestsBase#AddApiHttpClients." + + "\n* To mock the client, use the generic AddApiHttpClients." + + "\n* To mock the server, change the client's BaseAddress." + + "\n*" + + "\n* 3. Locate the test you want below" + + "\n* - remove the skip property from the Fact attribute" + + "\n* - set the value of any variables if necessary" + + "\n*" + + "\n* 4. Run the tests and ensure they work." + + "\n*" + + "\n*/"); + } + } + public void addRestSharpSupportingFiles(final String clientPackageDir, final String packageFolder, + final AtomicReference excludeTests, final String testPackageFolder, final String testPackageName, final String modelPackageDir){ supportingFiles.add(new SupportingFile("IApiAccessor.mustache", clientPackageDir, "IApiAccessor.cs")); supportingFiles.add(new SupportingFile("Configuration.mustache", clientPackageDir, "Configuration.cs")); supportingFiles.add(new SupportingFile("ApiClient.mustache", clientPackageDir, "ApiClient.cs")); @@ -708,12 +786,6 @@ public void processOpts() { supportingFiles.add(new SupportingFile("GlobalConfiguration.mustache", clientPackageDir, "GlobalConfiguration.cs")); - // Only write out test related files if excludeTests is unset or explicitly set to false (see start of this method) - if (Boolean.FALSE.equals(excludeTests.get())) { - modelTestTemplateFiles.put("model_test.mustache", ".cs"); - apiTestTemplateFiles.put("api_test.mustache", ".cs"); - } - supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh")); supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); @@ -727,9 +799,64 @@ public void processOpts() { supportingFiles.add(new SupportingFile("appveyor.mustache", "", "appveyor.yml")); supportingFiles.add(new SupportingFile("AbstractOpenAPISchema.mustache", modelPackageDir, "AbstractOpenAPISchema.cs")); + } + + public void addGenericHostSupportingFiles(final String clientPackageDir, final String packageFolder, + final AtomicReference excludeTests, final String testPackageFolder, final String testPackageName, final String modelPackageDir){ + supportingFiles.add(new SupportingFile("TokenProvider`1.mustache", clientPackageDir, "TokenProvider`1.cs")); + supportingFiles.add(new SupportingFile("RateLimitProvider`1.mustache", clientPackageDir, "RateLimitProvider`1.cs")); + supportingFiles.add(new SupportingFile("TokenContainer`1.mustache", clientPackageDir, "TokenContainer`1.cs")); + supportingFiles.add(new SupportingFile("TokenBase.mustache", clientPackageDir, "TokenBase.cs")); + supportingFiles.add(new SupportingFile("ApiException.mustache", clientPackageDir, "ApiException.cs")); + supportingFiles.add(new SupportingFile("ApiResponse`1.mustache", clientPackageDir, "ApiResponse`1.cs")); + supportingFiles.add(new SupportingFile("ClientUtils.mustache", clientPackageDir, "ClientUtils.cs")); + supportingFiles.add(new SupportingFile("HostConfiguration.mustache", clientPackageDir, "HostConfiguration.cs")); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("git_push.sh.mustache", "docs" + File.separator + "scripts", "git_push.sh")); + supportingFiles.add(new SupportingFile("git_push.ps1.mustache", "docs" + File.separator + "scripts", "git_push.ps1")); + // TODO: supportingFiles.add(new SupportingFile("generate.ps1.mustache", "docs" + File.separator + "scripts", "generate.ps1")); + supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); + supportingFiles.add(new SupportingFile("Solution.mustache", "", packageName + ".sln")); + supportingFiles.add(new SupportingFile("netcore_project.mustache", packageFolder, packageName + ".csproj")); + supportingFiles.add(new SupportingFile("appveyor.mustache", "", "appveyor.yml")); + supportingFiles.add(new SupportingFile("AbstractOpenAPISchema.mustache", modelPackageDir, "AbstractOpenAPISchema.cs")); + supportingFiles.add(new SupportingFile("OpenAPIDateConverter.mustache", clientPackageDir, "OpenAPIDateConverter.cs")); + supportingFiles.add(new SupportingFile("ApiResponseEventArgs.mustache", clientPackageDir, "ApiResponseEventArgs.cs")); + supportingFiles.add(new SupportingFile("IApi.mustache", clientPackageDir, getInterfacePrefix() + "Api.cs")); - additionalProperties.put("apiDocPath", apiDocPath); - additionalProperties.put("modelDocPath", modelDocPath); + String apiTestFolder = testFolder + File.separator + testPackageName() + File.separator + apiPackage(); + + if (Boolean.FALSE.equals(excludeTests.get())) { + supportingFiles.add(new SupportingFile("netcore_testproject.mustache", testPackageFolder, testPackageName + ".csproj")); + supportingFiles.add(new SupportingFile("DependencyInjectionTests.mustache", apiTestFolder, "DependencyInjectionTests.cs")); + + // do not overwrite test file that already exists + File apiTestsBaseFile = new File(apiTestFileFolder() + File.separator + "ApiTestsBase.cs"); + if (!apiTestsBaseFile.exists()) { + supportingFiles.add(new SupportingFile("ApiTestsBase.mustache", apiTestFolder, "ApiTestsBase.cs")); + } + } + + if (ProcessUtils.hasHttpSignatureMethods(openAPI)) { + supportingFiles.add(new SupportingFile("HttpSigningConfiguration.mustache", clientPackageDir, "HttpSigningConfiguration.cs")); + supportingFiles.add(new SupportingFile("HttpSigningToken.mustache", clientPackageDir, "HttpSigningToken.cs")); + } + + if (ProcessUtils.hasHttpBasicMethods(openAPI)) { + supportingFiles.add(new SupportingFile("BasicToken.mustache", clientPackageDir, "BasicToken.cs")); + } + + if (ProcessUtils.hasOAuthMethods(openAPI)) { + supportingFiles.add(new SupportingFile("OAuthToken.mustache", clientPackageDir, "OAuthToken.cs")); + } + + if (ProcessUtils.hasHttpBearerMethods(openAPI)) { + supportingFiles.add(new SupportingFile("BearerToken.mustache", clientPackageDir, "BearerToken.cs")); + } + + if (ProcessUtils.hasApiKeyMethods(openAPI)) { + supportingFiles.add(new SupportingFile("ApiKeyToken.mustache", clientPackageDir, "ApiKeyToken.cs")); + } } public void setNetStandard(Boolean netStandard) { @@ -756,11 +883,24 @@ public void setPackageGuid(String packageGuid) { this.packageGuid = packageGuid; } + // TODO: this does the same as super @Override public void setPackageName(String packageName) { this.packageName = packageName; } + /** + * Sets the api name. This value must be a valid class name. + * @param apiName The api name + */ + public void setApiName(String apiName) { + if (!"".equals(apiName) && (Boolean.FALSE.equals(apiName.matches("^[a-zA-Z0-9_]*$")) || Boolean.FALSE.equals(apiName.matches("^[a-zA-Z].*")))){ + throw new RuntimeException("Invalid project name " + apiName + ". May only contain alphanumeric characaters or underscore and start with a letter."); + } + this.apiName = apiName; + } + + // TODO: this does the same as super @Override public void setPackageVersion(String packageVersion) { this.packageVersion = packageVersion; @@ -1068,6 +1208,9 @@ protected void configureAdditionalPropertiesForFrameworks(final Map Boolean.TRUE.equals(p.isNetStandard))); + for (FrameworkStrategy frameworkStrategy : frameworkStrategies) { + properties.put(frameworkStrategy.name, strategies.stream().anyMatch(s -> s.name.equals(frameworkStrategy.name))); + } } /** diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/api_doc.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/api_doc.mustache index 1286829ad402..3a3870510c94 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/api_doc.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/api_doc.mustache @@ -128,7 +128,7 @@ Name | Type | Description | Notes {{/responses}} {{/responses.0}} -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +[[Back to top]](#) [[Back to API list]](../{{#useGenericHost}}../{{/useGenericHost}}README.md#documentation-for-api-endpoints) [[Back to Model list]](../{{#useGenericHost}}../{{/useGenericHost}}README.md#documentation-for-models) [[Back to README]](../{{#useGenericHost}}../{{/useGenericHost}}README.md) {{/operation}} {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiException.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiException.mustache new file mode 100644 index 000000000000..77e95ca3d144 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiException.mustache @@ -0,0 +1,44 @@ +// +{{>partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; + +namespace {{packageName}}.Client +{ + /// + /// API Exception + /// + {{>visibility}} class ApiException : Exception + { + /// + /// The reason the api request failed + /// + public string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ReasonPhrase { get; } + + /// + /// The HttpStatusCode + /// + public System.Net.HttpStatusCode StatusCode { get; } + + /// + /// The raw data returned by the api + /// + public string RawContent { get; } + + /// + /// Construct the ApiException from parts of the reponse + /// + /// + /// + /// + public ApiException(string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} reasonPhrase, System.Net.HttpStatusCode statusCode, string rawContent) : base(reasonPhrase ?? rawContent) + { + ReasonPhrase = reasonPhrase; + + StatusCode = statusCode; + + RawContent = rawContent; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiKeyToken.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiKeyToken.mustache new file mode 100644 index 000000000000..98434c3b25a2 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiKeyToken.mustache @@ -0,0 +1,59 @@ +// +{{partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; + +namespace {{packageName}}.Client +{ + /// + /// A token constructed from an apiKey. + /// + public class ApiKeyToken : TokenBase + { + private string _raw; + + /// + /// Constructs an ApiKeyToken object. + /// + /// + /// + /// + public ApiKeyToken(string value, string prefix = "Bearer ", TimeSpan? timeout = null) : base(timeout) + { + _raw = $"{ prefix }{ value }"; + } + + /// + /// Places the token in the cookie. + /// + /// + /// + public virtual void UseInCookie(System.Net.Http.HttpRequestMessage request, string cookieName) + { + request.Headers.Add("Cookie", $"{ cookieName }=_raw"); + } + + /// + /// Places the token in the header. + /// + /// + /// + public virtual void UseInHeader(System.Net.Http.HttpRequestMessage request, string headerName) + { + request.Headers.Add(headerName, _raw); + } + + /// + /// Places the token in the query. + /// + /// + /// + /// + /// + public virtual void UseInQuery(System.Net.Http.HttpRequestMessage request, UriBuilder uriBuilder, System.Collections.Specialized.NameValueCollection parseQueryString, string parameterName) + { + parseQueryString[parameterName] = Uri.EscapeDataString(_raw).ToString(){{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}; + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponseEventArgs.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponseEventArgs.mustache new file mode 100644 index 000000000000..5ff16199326e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponseEventArgs.mustache @@ -0,0 +1,47 @@ +using System; +using System.Net; + +namespace {{packageName}}.Client +{ + /// + /// Useful for tracking server health. + /// + public class ApiResponseEventArgs : EventArgs + { + /// + /// The time the request was sent. + /// + public DateTime RequestedAt { get; } + /// + /// The time the response was received. + /// + public DateTime ReceivedAt { get; } + /// + /// The HttpStatusCode received. + /// + public HttpStatusCode HttpStatus { get; } + /// + /// The path requested. + /// + public string Path { get; } + /// + /// The elapsed time from request to response. + /// + public TimeSpan ToTimeSpan => this.ReceivedAt - this.RequestedAt; + + /// + /// The event args used to track server health. + /// + /// + /// + /// + /// + public ApiResponseEventArgs(DateTime requestedAt, DateTime receivedAt, HttpStatusCode httpStatus, string path) + { + RequestedAt = requestedAt; + ReceivedAt = receivedAt; + HttpStatus = httpStatus; + Path = path; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponse`1.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponse`1.mustache new file mode 100644 index 000000000000..ce8b308e9caf --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiResponse`1.mustache @@ -0,0 +1,97 @@ +// +{{>partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Collections.Generic; +using System.Net; +using Newtonsoft.Json; + +namespace {{packageName}}.Client +{ + /// + /// Provides a non-generic contract for the ApiResponse wrapper. + /// + public interface IApiResponse + { + /// + /// The data type of + /// + Type ResponseType { get; } + + /// + /// Gets or sets the status code (HTTP status code) + /// + /// The status code. + HttpStatusCode StatusCode { get; } + + /// + /// The raw content of this response + /// + string RawContent { get; } + } + + /// + /// API Response + /// + {{>visibility}} partial class ApiResponse : IApiResponse + { + #region Properties + + /// + /// The deserialized content + /// + {{! .net 3.1 does not support unconstrained nullable T }} + public T{{#nullableReferenceTypes}}{{^netcoreapp3.1}}?{{/netcoreapp3.1}}{{/nullableReferenceTypes}} Content { get; set; } + + /// + /// Gets or sets the status code (HTTP status code) + /// + /// The status code. + public HttpStatusCode StatusCode { get; } + + /// + /// The content of this response + /// + public Type ResponseType + { + get { return typeof(T); } + } + + /// + /// The raw data + /// + public string RawContent { get; } + + /// + /// The IsSuccessStatusCode from the api response + /// + public bool IsSuccessStatusCode { get; } + + /// + /// The reason phrase contained in the api response + /// + public string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ReasonPhrase { get; } + + /// + /// The headers contained in the api response + /// + public System.Net.Http.Headers.HttpResponseHeaders Headers { get; } + + #endregion Properties + + /// + /// Construct the reponse using an HttpResponseMessage + /// + /// + /// + public ApiResponse(System.Net.Http.HttpResponseMessage response, string rawContent) + { + StatusCode = response.StatusCode; + Headers = response.Headers; + IsSuccessStatusCode = response.IsSuccessStatusCode; + ReasonPhrase = response.ReasonPhrase; + RawContent = rawContent; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiTestsBase.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiTestsBase.mustache new file mode 100644 index 000000000000..57dd3c7edee2 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ApiTestsBase.mustache @@ -0,0 +1,47 @@ +{{>partial_header}} +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using Microsoft.Extensions.Hosting; +using {{packageName}}.Client;{{#hasImport}} +using {{packageName}}.{{modelPackage}};{{/hasImport}} + + +{{{testInstructions}}} + + +namespace {{packageName}}.Test.Api +{ + /// + /// Base class for API tests + /// + public class ApiTestsBase + { + protected readonly IHost _host; + + public ApiTestsBase(string[] args) + { + _host = CreateHostBuilder(args).Build(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .Configure{{apiName}}((context, options) => + { + {{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken(context.Configuration[""], timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(apiKeyToken); + {{/hasApiKeyMethods}}{{#hasHttpBearerMethods}} + BearerToken bearerToken = new BearerToken(context.Configuration[""], timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(bearerToken); + {{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}} + BasicToken basicToken = new BasicToken(context.Configuration[""], context.Configuration[""], timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(basicToken); + {{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}} + HttpSigningConfiguration config = new HttpSigningConfiguration("", "", null, new List(), HashAlgorithmName.SHA256, "", 0); + HttpSignatureToken httpSignatureToken = new HttpSignatureToken(config, timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(httpSignatureToken); + {{/hasHttpSignatureMethods}}{{#hasOAuthMethods}} + OAuthToken oauthToken = new OAuthToken(context.Configuration[""], timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(oauthToken);{{/hasOAuthMethods}} + }); + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BasicToken.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BasicToken.mustache new file mode 100644 index 000000000000..78fce754c171 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BasicToken.mustache @@ -0,0 +1,44 @@ +// +{{partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace {{packageName}}.Client +{ + /// + /// A token constructed from a username and password. + /// + public class BasicToken : TokenBase + { + private string _username; + + private string _password; + + /// + /// Constructs a BasicToken object. + /// + /// + /// + /// + public BasicToken(string username, string password, TimeSpan? timeout = null) : base(timeout) + { + _username = username; + + _password = password; + } + + /// + /// Places the token in the header. + /// + /// + /// + public virtual void UseInHeader(System.Net.Http.HttpRequestMessage request, string headerName) + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", {{packageName}}.Client.ClientUtils.Base64Encode(_username + ":" + _password)); + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BearerToken.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BearerToken.mustache new file mode 100644 index 000000000000..0e6a8dbdf231 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/BearerToken.mustache @@ -0,0 +1,39 @@ +// +{{partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace {{packageName}}.Client +{ + /// + /// A token constructed from a token from a bearer token. + /// + public class BearerToken : TokenBase + { + private string _raw; + + /// + /// Constructs a BearerToken object. + /// + /// + /// + public BearerToken(string value, TimeSpan? timeout = null) : base(timeout) + { + _raw = value; + } + + /// + /// Places the token in the header. + /// + /// + /// + public virtual void UseInHeader(System.Net.Http.HttpRequestMessage request, string headerName) + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _raw); + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ClientUtils.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ClientUtils.mustache new file mode 100644 index 000000000000..b3b398425e43 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/ClientUtils.mustache @@ -0,0 +1,360 @@ +{{>partial_header}} + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection;{{#supportsRetry}} +using Polly.Timeout; +using Polly.Extensions.Http; +using Polly;{{/supportsRetry}} +using System.Net.Http; +using {{packageName}}.Api;{{#useCompareNetObjects}} +using KellermanSoftware.CompareNetObjects;{{/useCompareNetObjects}} + +namespace {{packageName}}.Client +{ + /// + /// Utility functions providing some benefit to API client consumers. + /// + public static class ClientUtils + { + {{#useCompareNetObjects}} + /// + /// An instance of CompareLogic. + /// + public static CompareLogic compareLogic; + + /// + /// Static constructor to initialise compareLogic. + /// + static ClientUtils() + { + compareLogic = new CompareLogic(); + } + {{/useCompareNetObjects}} + + /// + /// A delegate for events. + /// + /// + /// + /// + /// + public delegate void EventHandler(object sender, T e) where T : EventArgs; + + /// + /// Custom JSON serializer + /// + public static Newtonsoft.Json.JsonSerializerSettings JsonSerializerSettings { get; set; } = new Newtonsoft.Json.JsonSerializerSettings + { + // OpenAPI generated types generally hide default constructors. + ConstructorHandling = Newtonsoft.Json.ConstructorHandling.AllowNonPublicDefaultConstructor, + MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Ignore, + ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver + { + NamingStrategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy + { + OverrideSpecifiedNames = false + } + } + }; + + /// + /// Sanitize filename by removing the path + /// + /// Filename + /// Filename + public static string SanitizeFilename(string filename) + { + Match match = Regex.Match(filename, @".*[/\\](.*)$"); + return match.Success ? match.Groups[1].Value : filename; + } + + /// + /// If parameter is DateTime, output in a formatted string (default ISO 8601), customizable with Configuration.DateTime. + /// If parameter is a list, join the list with ",". + /// Otherwise just return the string. + /// + /// The parameter (header, path, query, form). + /// The DateTime serialization format. + /// Formatted string. + public static string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ParameterToString(object obj, string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} format = ISO8601_DATETIME_FORMAT) + { + if (obj is DateTime dateTime) + // Return a formatted date string - Can be customized with Configuration.DateTimeFormat + // Defaults to an ISO 8601, using the known as a Round-trip date/time pattern ("o") + // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#Anchor_8 + // For example: 2009-06-15T13:45:30.0000000 + return dateTime.ToString(format); + if (obj is DateTimeOffset dateTimeOffset) + // Return a formatted date string - Can be customized with Configuration.DateTimeFormat + // Defaults to an ISO 8601, using the known as a Round-trip date/time pattern ("o") + // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#Anchor_8 + // For example: 2009-06-15T13:45:30.0000000 + return dateTimeOffset.ToString(format); + if (obj is bool boolean) + return boolean ? "true" : "false"; + if (obj is System.Collections.ICollection collection) + return string.Join(",", collection.Cast()); + + return Convert.ToString(obj, System.Globalization.CultureInfo.InvariantCulture); + } + + /// + /// URL encode a string + /// Credit/Ref: https://github.com/restsharp/RestSharp/blob/master/RestSharp/Extensions/StringExtensions.cs#L50 + /// + /// string to be URL encoded + /// Byte array + public static string UrlEncode(string input) + { + const int maxLength = 32766; + + if (input == null) + { + throw new ArgumentNullException("input"); + } + + if (input.Length <= maxLength) + { + return Uri.EscapeDataString(input); + } + + StringBuilder sb = new StringBuilder(input.Length * 2); + int index = 0; + + while (index < input.Length) + { + int length = Math.Min(input.Length - index, maxLength); + string subString = input.Substring(index, length); + + sb.Append(Uri.EscapeDataString(subString)); + index += subString.Length; + } + + return sb.ToString(); + } + + /// + /// Encode string in base64 format. + /// + /// string to be encoded. + /// Encoded string. + public static string Base64Encode(string text) + { + return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(text)); + } + + /// + /// Convert stream to byte array + /// + /// Input stream to be converted + /// Byte array + public static byte[] ReadAsBytes(Stream inputStream) + { + using (var ms = new MemoryStream()) + { + inputStream.CopyTo(ms); + return ms.ToArray(); + } + } + + /// + /// Select the Content-Type header's value from the given content-type array: + /// if JSON type exists in the given array, use it; + /// otherwise use the first one defined in 'consumes' + /// + /// The Content-Type array to select from. + /// The Content-Type header to use. + public static string SelectHeaderContentType(string[] contentTypes) + { + if (contentTypes.Length == 0) + return null; + + foreach (var contentType in contentTypes) + { + if (IsJsonMime(contentType)) + return contentType; + } + + return contentTypes[0]; // use the first content type specified in 'consumes' + } + + /// + /// Select the Accept header's value from the given accepts array: + /// if JSON exists in the given array, use it; + /// otherwise use all of them (joining into a string) + /// + /// The accepts array to select from. + /// The Accept header to use. + public static string SelectHeaderAccept(string[] accepts) + { + if (accepts.Length == 0) + return null; + + if (accepts.Contains("application/json", StringComparer.OrdinalIgnoreCase)) + return "application/json"; + + return string.Join(",", accepts); + } + + /// + /// Provides a case-insensitive check that a provided content type is a known JSON-like content type. + /// + public static readonly Regex JsonRegex = new Regex("(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"); + + /// + /// Check if the given MIME is a JSON MIME. + /// JSON MIME examples: + /// application/json + /// application/json; charset=UTF8 + /// APPLICATION/JSON + /// application/vnd.company+json + /// + /// MIME + /// Returns True if MIME type is json. + public static bool IsJsonMime(string mime) + { + if (string.IsNullOrWhiteSpace(mime)) return false; + + return JsonRegex.IsMatch(mime) || mime.Equals("application/json-patch+json"); + } + + /// + /// The base path of the API + /// + public const string BASE_ADDRESS = "{{{basePath}}}"; + + /// + /// The scheme of the API + /// + public const string SCHEME = "{{{scheme}}}"; + + /// + /// The context path of the API + /// + public const string CONTEXT_PATH = "{{contextPath}}"; + + /// + /// The host of the API + /// + public const string HOST = "{{{host}}}"; + + /// + /// The format to use for DateTime serialization + /// + public const string ISO8601_DATETIME_FORMAT = "o"; + + /// + /// Add the api to your host builder. + /// + /// + /// + public static IHostBuilder Configure{{apiName}}(this IHostBuilder builder, Action options) + { + builder.ConfigureServices((context, services) => + { + HostConfiguration config = new HostConfiguration(services); + + options(context, config); + + Add{{apiName}}(services, config); + }); + + return builder; + } + + /// + /// Add the api to your host builder. + /// + /// + /// + public static void Add{{apiName}}(this IServiceCollection services, Action options) + { + HostConfiguration config = new HostConfiguration(services); + options(config); + Add{{apiName}}(services, config); + } + + private static void Add{{apiName}}(IServiceCollection services, HostConfiguration host) + { + if (!host.HttpClientsAdded) + host.Add{{apiName}}HttpClients(); + + // ensure that a token provider was provided for this token type + // if not, default to RateLimitProvider + var containerServices = services.Where(s => s.ServiceType.IsGenericType && + s.ServiceType.GetGenericTypeDefinition().IsAssignableFrom(typeof(TokenContainer<>))).ToArray(); + + foreach(var containerService in containerServices) + { + var tokenType = containerService.ServiceType.GenericTypeArguments[0]; + + var provider = services.FirstOrDefault(s => s.ServiceType.IsAssignableFrom(typeof(TokenProvider<>).MakeGenericType(tokenType))); + + if (provider == null) + { + services.AddSingleton(typeof(RateLimitProvider<>).MakeGenericType(tokenType)); + services.AddSingleton(typeof(TokenProvider<>).MakeGenericType(tokenType), + s => s.GetRequiredService(typeof(RateLimitProvider<>).MakeGenericType(tokenType))); + } + } + }{{#supportsRetry}} + + /// + /// Adds a Polly retry policy to your clients. + /// + /// + /// + /// + public static IHttpClientBuilder AddRetryPolicy(this IHttpClientBuilder client, int retries) + { + client.AddPolicyHandler(RetryPolicy(retries)); + + return client; + } + + /// + /// Adds a Polly timeout policy to your clients. + /// + /// + /// + /// + public static IHttpClientBuilder AddTimeoutPolicy(this IHttpClientBuilder client, TimeSpan timeout) + { + client.AddPolicyHandler(TimeoutPolicy(timeout)); + + return client; + } + + /// + /// Adds a Polly circiut breaker to your clients. + /// + /// + /// + /// + /// + public static IHttpClientBuilder AddCircuitBreakerPolicy(this IHttpClientBuilder client, int handledEventsAllowedBeforeBreaking, TimeSpan durationOfBreak) + { + client.AddTransientHttpErrorPolicy(builder => CircuitBreakerPolicy(builder, handledEventsAllowedBeforeBreaking, durationOfBreak)); + + return client; + } + + private static Polly.Retry.AsyncRetryPolicy RetryPolicy(int retries) + => HttpPolicyExtensions + .HandleTransientHttpError() + .Or() + .RetryAsync(retries); + + private static AsyncTimeoutPolicy TimeoutPolicy(TimeSpan timeout) + => Policy.TimeoutAsync(timeout); + + private static Polly.CircuitBreaker.AsyncCircuitBreakerPolicy CircuitBreakerPolicy( + PolicyBuilder builder, int handledEventsAllowedBeforeBreaking, TimeSpan durationOfBreak) + => builder.CircuitBreakerAsync(handledEventsAllowedBeforeBreaking, durationOfBreak);{{/supportsRetry}} + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/DependencyInjectionTests.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/DependencyInjectionTests.mustache new file mode 100644 index 000000000000..ac4a4d8e2d11 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/DependencyInjectionTests.mustache @@ -0,0 +1,158 @@ +{{>partial_header}} +using System; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Security.Cryptography; +using {{packageName}}.Client; +using {{packageName}}.{{apiPackage}}; +using Xunit; + +namespace {{packageName}}.Test.Api +{ + /// + /// Tests the dependency injection. + /// + public class DependencyInjectionTest + { + private readonly IHost _hostUsingConfigureWithoutAClient = + Host.CreateDefaultBuilder(Array.Empty()).Configure{{apiName}}((context, options) => + { + {{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(apiKeyToken); + {{/hasApiKeyMethods}}{{#hasHttpBearerMethods}} + BearerToken bearerToken = new BearerToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(bearerToken); + {{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}} + BasicToken basicToken = new BasicToken("", "", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(basicToken); + {{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}} + HttpSigningConfiguration config = new HttpSigningConfiguration("", "", null, new List(), HashAlgorithmName.SHA256, "", 0); + HttpSignatureToken httpSignatureToken = new HttpSignatureToken(config, timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(httpSignatureToken); + {{/hasHttpSignatureMethods}}{{#hasOAuthMethods}} + OAuthToken oauthToken = new OAuthToken("token", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(oauthToken);{{/hasOAuthMethods}} + }) + .Build(); + + private readonly IHost _hostUsingConfigureWithAClient = + Host.CreateDefaultBuilder(Array.Empty()).Configure{{apiName}}((context, options) => + { + {{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(apiKeyToken); + {{/hasApiKeyMethods}}{{#hasHttpBearerMethods}} + BearerToken bearerToken = new BearerToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(bearerToken); + {{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}} + BasicToken basicToken = new BasicToken("", "", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(basicToken); + {{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}} + HttpSigningConfiguration config = new HttpSigningConfiguration("", "", null, new List(), HashAlgorithmName.SHA256, "", 0); + HttpSignatureToken httpSignatureToken = new HttpSignatureToken(config, timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(httpSignatureToken); + {{/hasHttpSignatureMethods}}{{#hasOAuthMethods}} + OAuthToken oauthToken = new OAuthToken("token", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(oauthToken);{{/hasOAuthMethods}} + options.Add{{apiName}}HttpClients(client => client.BaseAddress = new Uri(ClientUtils.BASE_ADDRESS)); + }) + .Build(); + + private readonly IHost _hostUsingAddWithoutAClient = + Host.CreateDefaultBuilder(Array.Empty()).ConfigureServices((host, services) => + { + services.Add{{apiName}}(options => + { + {{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(apiKeyToken); + {{/hasApiKeyMethods}}{{#hasHttpBearerMethods}} + BearerToken bearerToken = new BearerToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(bearerToken); + {{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}} + BasicToken basicToken = new BasicToken("", "", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(basicToken); + {{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}} + HttpSigningConfiguration config = new HttpSigningConfiguration("", "", null, new List(), HashAlgorithmName.SHA256, "", 0); + HttpSignatureToken httpSignatureToken = new HttpSignatureToken(config, timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(httpSignatureToken); + {{/hasHttpSignatureMethods}}{{#hasOAuthMethods}} + OAuthToken oauthToken = new OAuthToken("token", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(oauthToken);{{/hasOAuthMethods}} + }); + }) + .Build(); + + private readonly IHost _hostUsingAddWithAClient = + Host.CreateDefaultBuilder(Array.Empty()).ConfigureServices((host, services) => + { + services.Add{{apiName}}(options => + { + {{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(apiKeyToken); + {{/hasApiKeyMethods}}{{#hasHttpBearerMethods}} + BearerToken bearerToken = new BearerToken($"", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(bearerToken); + {{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}} + BasicToken basicToken = new BasicToken("", "", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(basicToken); + {{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}} + HttpSigningConfiguration config = new HttpSigningConfiguration("", "", null, new List(), HashAlgorithmName.SHA256, "", 0); + HttpSignatureToken httpSignatureToken = new HttpSignatureToken(config, timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(httpSignatureToken); + {{/hasHttpSignatureMethods}}{{#hasOAuthMethods}} + OAuthToken oauthToken = new OAuthToken("token", timeout: TimeSpan.FromSeconds(1)); + options.AddTokens(oauthToken);{{/hasOAuthMethods}} + options.Add{{apiName}}HttpClients(client => client.BaseAddress = new Uri(ClientUtils.BASE_ADDRESS)); + }); + }) + .Build(); + + /// + /// Test dependency injection when using the configure method + /// + [Fact] + public void ConfigureApiWithAClientTest() + { + {{#apiInfo}}{{#apis}}var {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}} = _hostUsingConfigureWithAClient.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>(); + Assert.True({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}.HttpClient.BaseAddress != null);{{^-last}} + + {{/-last}}{{/apis}}{{/apiInfo}} + } + + /// + /// Test dependency injection when using the configure method + /// + [Fact] + public void ConfigureApiWithoutAClientTest() + { + {{#apiInfo}}{{#apis}}var {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}} = _hostUsingConfigureWithoutAClient.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>(); + Assert.True({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}.HttpClient.BaseAddress != null);{{^-last}} + + {{/-last}}{{/apis}}{{/apiInfo}} + } + + /// + /// Test dependency injection when using the add method + /// + [Fact] + public void AddApiWithAClientTest() + { + {{#apiInfo}}{{#apis}}var {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}} = _hostUsingAddWithAClient.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>(); + Assert.True({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}.HttpClient.BaseAddress != null);{{^-last}} + + {{/-last}}{{/apis}}{{/apiInfo}} + } + + /// + /// Test dependency injection when using the add method + /// + [Fact] + public void AddApiWithoutAClientTest() + { + {{#apiInfo}}{{#apis}}var {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}} = _hostUsingAddWithoutAClient.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>(); + Assert.True({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}.HttpClient.BaseAddress != null);{{^-last}} + + {{/-last}}{{/apis}}{{/apiInfo}} + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HostConfiguration.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HostConfiguration.mustache new file mode 100644 index 000000000000..24530537f967 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HostConfiguration.mustache @@ -0,0 +1,124 @@ +{{>partial_header}} + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using {{packageName}}.Api; + +namespace {{packageName}}.Client +{ + /// + /// Provides hosting configuration for {{packageName}} + /// + public class HostConfiguration + { + private readonly IServiceCollection _services; + internal bool HttpClientsAdded { get; private set; } + + /// + /// Instantiates the class + /// + /// + public HostConfiguration(IServiceCollection services) + { + _services = services;{{#apiInfo}}{{#apis}} + services.AddSingleton<{{interfacePrefix}}{{classname}}, {{classname}}>();{{/apis}}{{/apiInfo}} + } + + /// + /// Configures the HttpClients. + /// + /// + /// + /// + public HostConfiguration Add{{apiName}}HttpClients<{{#apiInfo}}{{#apis}}T{{classname}}{{^-last}}, {{/-last}}{{/apis}}> + ( + Action{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} client = null, Action{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} builder = null){{#apis}} + where T{{classname}} : class, {{interfacePrefix}}{{classname}}{{/apis}} + { + if (client == null) + client = c => c.BaseAddress = new Uri(ClientUtils.BASE_ADDRESS); + + List builders = new List(); + + {{#apis}}builders.Add(_services.AddHttpClient<{{interfacePrefix}}{{classname}}, T{{classname}}>(client)); + {{/apis}}{{/apiInfo}} + if (builder != null) + foreach (IHttpClientBuilder instance in builders) + builder(instance); + + HttpClientsAdded = true; + + return this; + } + + /// + /// Configures the HttpClients. + /// + /// + /// + /// + public HostConfiguration Add{{apiName}}HttpClients( + Action{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} client = null, Action{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} builder = null) + { + Add{{apiName}}HttpClients<{{#apiInfo}}{{#apis}}{{classname}}{{^-last}}, {{/-last}}{{/apis}}{{/apiInfo}}>(client, builder); + + return this; + } + + /// + /// Configures the JsonSerializerSettings + /// + /// + /// + public HostConfiguration ConfigureJsonOptions(Action options) + { + options(Client.ClientUtils.JsonSerializerSettings); + + return this; + } + + /// + /// Adds tokens to your IServiceCollection + /// + /// + /// + /// + public HostConfiguration AddTokens(TTokenBase token) where TTokenBase : TokenBase + { + return AddTokens(new TTokenBase[]{ token }); + } + + /// + /// Adds tokens to your IServiceCollection + /// + /// + /// + /// + public HostConfiguration AddTokens(IEnumerable tokens) where TTokenBase : TokenBase + { + TokenContainer container = new TokenContainer(tokens); + _services.AddSingleton(services => container); + + return this; + } + + /// + /// Adds a token provider to your IServiceCollection + /// + /// + /// + /// + public HostConfiguration UseProvider() + where TTokenProvider : TokenProvider + where TTokenBase : TokenBase + { + _services.AddSingleton(); + _services.AddSingleton>(services => services.GetRequiredService()); + + return this; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningConfiguration.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningConfiguration.mustache new file mode 100644 index 000000000000..23b2d9149a1d --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningConfiguration.mustache @@ -0,0 +1,676 @@ +// +{{>partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Web; + +namespace {{packageName}}.Client +{ + /// + /// Class for HttpSigning auth related parameter and methods + /// + public class HttpSigningConfiguration + { + #region + /// + /// Create an instance + /// + public HttpSigningConfiguration(string keyId, string keyFilePath, SecureString{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} keyPassPhrase, List httpSigningHeader, HashAlgorithmName hashAlgorithm, string signingAlgorithm, int signatureValidityPeriod) + { + KeyId = keyId; + KeyFilePath = keyFilePath; + KeyPassPhrase = keyPassPhrase; + HttpSigningHeader = httpSigningHeader; + HashAlgorithm = hashAlgorithm; + SigningAlgorithm = signingAlgorithm; + SignatureValidityPeriod = signatureValidityPeriod; + } + #endregion + + #region Properties + /// + ///Gets the Api keyId + /// + public string KeyId { get; set; } + + /// + /// Gets the Key file path + /// + public string KeyFilePath { get; set; } + + /// + /// Gets the key pass phrase for password protected key + /// + public SecureString{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} KeyPassPhrase { get; set; } + + /// + /// Gets the HTTP signing header + /// + public List HttpSigningHeader { get; set; } + + /// + /// Gets the hash algorithm sha256 or sha512 + /// + public HashAlgorithmName HashAlgorithm { get; set; } = HashAlgorithmName.SHA256; + + /// + /// Gets the signing algorithm + /// + public string SigningAlgorithm { get; set; } + + /// + /// Gets the Signature validaty period in seconds + /// + public int SignatureValidityPeriod { get; set; } + + #endregion + + #region enum + private enum PrivateKeyType + { + None = 0, + RSA = 1, + ECDSA = 2, + } + #endregion + + #region Methods + /// + /// Gets the Headers for HttpSigning + /// + /// + /// + /// + internal Dictionary GetHttpSignedHeader(System.Net.Http.HttpRequestMessage request, string requestBody, System.Threading.CancellationToken? cancellationToken = null) + { + if (request.RequestUri == null) + throw new NullReferenceException("The request URI was null"); + + const string HEADER_REQUEST_TARGET = "(request-target)"; + + // The time when the HTTP signature expires. The API server should reject HTTP requests that have expired. + const string HEADER_EXPIRES = "(expires)"; + + //The 'Date' header. + const string HEADER_DATE = "Date"; + + //The 'Host' header. + const string HEADER_HOST = "Host"; + + //The time when the HTTP signature was generated. + const string HEADER_CREATED = "(created)"; + + //When the 'Digest' header is included in the HTTP signature, the client automatically + //computes the digest of the HTTP request body, per RFC 3230. + const string HEADER_DIGEST = "Digest"; + + //The 'Authorization' header is automatically generated by the client. It includes + //the list of signed headers and a base64-encoded signature. + const string HEADER_AUTHORIZATION = "Authorization"; + + //Hash table to store singed headers + var HttpSignedRequestHeader = new Dictionary(); + + var httpSignatureHeader = new Dictionary(); + + if (HttpSigningHeader.Count == 0) + HttpSigningHeader.Add("(created)"); + + var dateTime = DateTime.Now; + string digest = String.Empty; + + if (HashAlgorithm == HashAlgorithmName.SHA256) + { + var bodyDigest = GetStringHash(HashAlgorithm.ToString(), requestBody); + digest = string.Format("SHA-256={0}", Convert.ToBase64String(bodyDigest)); + } + else if (HashAlgorithm == HashAlgorithmName.SHA512) + { + var bodyDigest = GetStringHash(HashAlgorithm.ToString(), requestBody); + digest = string.Format("SHA-512={0}", Convert.ToBase64String(bodyDigest)); + } + else + throw new Exception(string.Format("{0} not supported", HashAlgorithm)); + + foreach (var header in HttpSigningHeader) + if (header.Equals(HEADER_REQUEST_TARGET)) + httpSignatureHeader.Add(header.ToLower(), request.RequestUri.ToString()); + else if (header.Equals(HEADER_EXPIRES)) + { + var expireDateTime = dateTime.AddSeconds(SignatureValidityPeriod); + httpSignatureHeader.Add(header.ToLower(), GetUnixTime(expireDateTime).ToString()); + } + else if (header.Equals(HEADER_DATE)) + { + var utcDateTime = dateTime.ToString("r").ToString(); + httpSignatureHeader.Add(header.ToLower(), utcDateTime); + HttpSignedRequestHeader.Add(HEADER_DATE, utcDateTime); + } + else if (header.Equals(HEADER_HOST)) + { + httpSignatureHeader.Add(header.ToLower(), request.RequestUri.ToString()); + HttpSignedRequestHeader.Add(HEADER_HOST, request.RequestUri.ToString()); + } + else if (header.Equals(HEADER_CREATED)) + httpSignatureHeader.Add(header.ToLower(), GetUnixTime(dateTime).ToString()); + else if (header.Equals(HEADER_DIGEST)) + { + HttpSignedRequestHeader.Add(HEADER_DIGEST, digest); + httpSignatureHeader.Add(header.ToLower(), digest); + } + else + { + bool isHeaderFound = false; + foreach (var item in request.Headers) + { + if (string.Equals(item.Key, header, StringComparison.OrdinalIgnoreCase)) + { + httpSignatureHeader.Add(header.ToLower(), item.Value.ToString()); + isHeaderFound = true; + break; + } + } + + if (!isHeaderFound) + throw new Exception(string.Format("Cannot sign HTTP request.Request does not contain the {0} header.",header)); + } + + var headersKeysString = String.Join(" ", httpSignatureHeader.Keys); + var headerValuesList = new List(); + + foreach (var keyVal in httpSignatureHeader) + headerValuesList.Add(string.Format("{0}: {1}", keyVal.Key, keyVal.Value)); + + //Concatinate headers value separated by new line + var headerValuesString = string.Join("\n", headerValuesList); + var signatureStringHash = GetStringHash(HashAlgorithm.ToString(), headerValuesString); + string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} headerSignatureStr = null; + var keyType = GetKeyType(KeyFilePath); + + if (keyType == PrivateKeyType.RSA) + headerSignatureStr = GetRSASignature(signatureStringHash); + + else if (keyType == PrivateKeyType.ECDSA) + headerSignatureStr = GetECDSASignature(signatureStringHash); + + var cryptographicScheme = "hs2019"; + var authorizationHeaderValue = string.Format("Signature keyId=\"{0}\",algorithm=\"{1}\"", + KeyId, cryptographicScheme); + + if (httpSignatureHeader.ContainsKey(HEADER_CREATED)) + authorizationHeaderValue += string.Format(",created={0}", httpSignatureHeader[HEADER_CREATED]); + + if (httpSignatureHeader.ContainsKey(HEADER_EXPIRES)) + authorizationHeaderValue += string.Format(",expires={0}", httpSignatureHeader[HEADER_EXPIRES]); + + authorizationHeaderValue += string.Format(",headers=\"{0}\",signature=\"{1}\"", headersKeysString, headerSignatureStr); + + HttpSignedRequestHeader.Add(HEADER_AUTHORIZATION, authorizationHeaderValue); + + return HttpSignedRequestHeader; + } + + private byte[] GetStringHash(string hashName, string stringToBeHashed) + { + var hashAlgorithm = System.Security.Cryptography.HashAlgorithm.Create(hashName); + + if (hashAlgorithm == null) + throw new NullReferenceException($"{ nameof(hashAlgorithm) } was null."); + + var bytes = Encoding.UTF8.GetBytes(stringToBeHashed); + var stringHash = hashAlgorithm.ComputeHash(bytes); + return stringHash; + } + + private int GetUnixTime(DateTime date2) + { + DateTime date1 = new DateTime(1970, 01, 01); + TimeSpan timeSpan = date2 - date1; + return (int)timeSpan.TotalSeconds; + } + + private string GetRSASignature(byte[] stringToSign) + { + if (KeyPassPhrase == null) + throw new NullReferenceException($"{ nameof(KeyPassPhrase) } was null."); + + RSA{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} rsa = GetRSAProviderFromPemFile(KeyFilePath, KeyPassPhrase); + + if (rsa == null) + return string.Empty; + else if (SigningAlgorithm == "RSASSA-PSS") + { + var signedbytes = rsa.SignHash(stringToSign, HashAlgorithm, RSASignaturePadding.Pss); + return Convert.ToBase64String(signedbytes); + } + else if (SigningAlgorithm == "PKCS1-v15") + { + var signedbytes = rsa.SignHash(stringToSign, HashAlgorithm, RSASignaturePadding.Pkcs1); + return Convert.ToBase64String(signedbytes); + } + + return string.Empty; + } + + /// + /// Gets the ECDSA signature + /// + /// + /// + private string GetECDSASignature(byte[] dataToSign) + { + if (!File.Exists(KeyFilePath)) + { + throw new Exception("key file path does not exist."); + } + + var ecKeyHeader = "-----BEGIN EC PRIVATE KEY-----"; + var ecKeyFooter = "-----END EC PRIVATE KEY-----"; + var keyStr = File.ReadAllText(KeyFilePath); + var ecKeyBase64String = keyStr.Replace(ecKeyHeader, "").Replace(ecKeyFooter, "").Trim(); + var keyBytes = System.Convert.FromBase64String(ecKeyBase64String); + var ecdsa = ECDsa.Create(); + +#if (NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0) + var byteCount = 0; + if (KeyPassPhrase != null) + { + IntPtr unmanagedString = IntPtr.Zero; + try + { + // convert secure string to byte array + unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(KeyPassPhrase); + + string ptrToStringUni = Marshal.PtrToStringUni(unmanagedString) ?? throw new NullReferenceException(); + + ecdsa.ImportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(ptrToStringUni), keyBytes, out byteCount); + } + finally + { + if (unmanagedString != IntPtr.Zero) + Marshal.ZeroFreeBSTR(unmanagedString); + } + } + else + { + ecdsa.ImportPkcs8PrivateKey(keyBytes, out byteCount); + } + var signedBytes = ecdsa.SignHash(dataToSign); + var derBytes = ConvertToECDSAANS1Format(signedBytes); + var signedString = System.Convert.ToBase64String(derBytes); + + return signedString; +#else + throw new Exception("ECDSA signing is supported only on NETCOREAPP3_0 and above"); +#endif + + } + + private byte[] ConvertToECDSAANS1Format(byte[] signedBytes) + { + var derBytes = new List(); + byte derLength = 68; //default lenght for ECDSA code signinged bit 0x44 + byte rbytesLength = 32; //R length 0x20 + byte sbytesLength = 32; //S length 0x20 + var rBytes = new List(); + var sBytes = new List(); + for (int i = 0; i < 32; i++) + rBytes.Add(signedBytes[i]); + + for (int i = 32; i < 64; i++) + sBytes.Add(signedBytes[i]); + + if (rBytes[0] > 0x7F) + { + derLength++; + rbytesLength++; + var tempBytes = new List(); + tempBytes.AddRange(rBytes); + rBytes.Clear(); + rBytes.Add(0x00); + rBytes.AddRange(tempBytes); + } + + if (sBytes[0] > 0x7F) + { + derLength++; + sbytesLength++; + var tempBytes = new List(); + tempBytes.AddRange(sBytes); + sBytes.Clear(); + sBytes.Add(0x00); + sBytes.AddRange(tempBytes); + + } + + derBytes.Add(48); //start of the sequence 0x30 + derBytes.Add(derLength); //total length r lenth, type and r bytes + + derBytes.Add(2); //tag for integer + derBytes.Add(rbytesLength); //length of r + derBytes.AddRange(rBytes); + + derBytes.Add(2); //tag for integer + derBytes.Add(sbytesLength); //length of s + derBytes.AddRange(sBytes); + return derBytes.ToArray(); + } + + private RSACryptoServiceProvider{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} GetRSAProviderFromPemFile(String pemfile, SecureString{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} keyPassPharse = null) + { + const String pempubheader = "-----BEGIN PUBLIC KEY-----"; + const String pempubfooter = "-----END PUBLIC KEY-----"; + bool isPrivateKeyFile = true; + byte[]{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} pemkey = null; + + if (!File.Exists(pemfile)) + throw new Exception("private key file does not exist."); + + string pemstr = File.ReadAllText(pemfile).Trim(); + + if (pemstr.StartsWith(pempubheader) && pemstr.EndsWith(pempubfooter)) + isPrivateKeyFile = false; + + if (isPrivateKeyFile) + { + pemkey = ConvertPrivateKeyToBytes(pemstr, keyPassPharse); + + if (pemkey == null) + return null; + + return DecodeRSAPrivateKey(pemkey); + } + return null; + } + + private byte[]{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ConvertPrivateKeyToBytes(String instr, SecureString{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} keyPassPharse = null) + { + const String pemprivheader = "-----BEGIN RSA PRIVATE KEY-----"; + const String pemprivfooter = "-----END RSA PRIVATE KEY-----"; + String pemstr = instr.Trim(); + byte[] binkey; + + if (!pemstr.StartsWith(pemprivheader) || !pemstr.EndsWith(pemprivfooter)) + return null; + + StringBuilder sb = new StringBuilder(pemstr); + sb.Replace(pemprivheader, ""); + sb.Replace(pemprivfooter, ""); + String pvkstr = sb.ToString().Trim(); + + try + { // if there are no PEM encryption info lines, this is an UNencrypted PEM private key + binkey = Convert.FromBase64String(pvkstr); + return binkey; + } + catch (System.FormatException) + { + StringReader str = new StringReader(pvkstr); + + //-------- read PEM encryption info. lines and extract salt ----- + if (!str.ReadLine(){{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}.StartsWith("Proc-Type: 4,ENCRYPTED")) // TODO: what do we do here if ReadLine is null? + return null; + + String saltline = str.ReadLine(){{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}; // TODO: what do we do here if ReadLine is null? + if (!saltline.StartsWith("DEK-Info: DES-EDE3-CBC,")) + return null; + + String saltstr = saltline.Substring(saltline.IndexOf(",") + 1).Trim(); + byte[] salt = new byte[saltstr.Length / 2]; + for (int i = 0; i < salt.Length; i++) + salt[i] = Convert.ToByte(saltstr.Substring(i * 2, 2), 16); + + if (!(str.ReadLine() == "")) + return null; + + //------ remaining b64 data is encrypted RSA key ---- + String encryptedstr = str.ReadToEnd(); + + try + { //should have b64 encrypted RSA key now + binkey = Convert.FromBase64String(encryptedstr); + } + catch (System.FormatException) + { //data is not in base64 fromat + return null; + } + + // TODO: what do we do here if keyPassPharse is null? + byte[] deskey = GetEncryptedKey(salt, keyPassPharse{{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}, 1, 2); // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes + if (deskey == null) + return null; + + //------ Decrypt the encrypted 3des-encrypted RSA private key ------ + byte[]{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} rsakey = DecryptKey(binkey, deskey, salt); //OpenSSL uses salt value in PEM header also as 3DES IV + + return rsakey; + } + } + + private RSACryptoServiceProvider{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} DecodeRSAPrivateKey(byte[] privkey) + { + byte[] MODULUS, E, D, P, Q, DP, DQ, IQ; + + // --------- Set up stream to decode the asn.1 encoded RSA private key ------ + MemoryStream mem = new MemoryStream(privkey); + BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading + byte bt = 0; + ushort twobytes = 0; + int elems = 0; + try + { + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes != 0x0102) //version number + return null; + + bt = binr.ReadByte(); + if (bt != 0x00) + return null; + + //------ all private key components are Integer sequences ---- + elems = GetIntegerSize(binr); + MODULUS = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + E = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + D = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + P = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + Q = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + DP = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + DQ = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + IQ = binr.ReadBytes(elems); + + // ------- create RSACryptoServiceProvider instance and initialize with public key ----- + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + RSAParameters RSAparams = new RSAParameters(); + RSAparams.Modulus = MODULUS; + RSAparams.Exponent = E; + RSAparams.D = D; + RSAparams.P = P; + RSAparams.Q = Q; + RSAparams.DP = DP; + RSAparams.DQ = DQ; + RSAparams.InverseQ = IQ; + RSA.ImportParameters(RSAparams); + return RSA; + } + catch (Exception) + { + return null; + } + finally + { + binr.Close(); + } + } + + private int GetIntegerSize(BinaryReader binr) + { + byte bt = 0; + byte lowbyte = 0x00; + byte highbyte = 0x00; + int count = 0; + bt = binr.ReadByte(); + if (bt != 0x02) //expect integer + return 0; + + bt = binr.ReadByte(); + + if (bt == 0x81) + count = binr.ReadByte(); // data size in next byte + else if (bt == 0x82) + { + highbyte = binr.ReadByte(); // data size in next 2 bytes + lowbyte = binr.ReadByte(); + byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; + count = BitConverter.ToInt32(modint, 0); + } + else + count = bt; // we already have the data size + + while (binr.ReadByte() == 0x00) + //remove high order zeros in data + count -= 1; + + binr.BaseStream.Seek(-1, SeekOrigin.Current); + + //last ReadByte wasn't a removed zero, so back up a byte + return count; + } + + private byte[] GetEncryptedKey(byte[] salt, SecureString secpswd, int count, int miter) + { + IntPtr unmanagedPswd = IntPtr.Zero; + int HASHLENGTH = 16; //MD5 bytes + byte[] keymaterial = new byte[HASHLENGTH * miter]; //to store concatenated Mi hashed results + + byte[] psbytes = new byte[secpswd.Length]; + unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi(secpswd); + Marshal.Copy(unmanagedPswd, psbytes, 0, psbytes.Length); + Marshal.ZeroFreeGlobalAllocAnsi(unmanagedPswd); + + // --- concatenate salt and pswd bytes into fixed data array --- + byte[] data00 = new byte[psbytes.Length + salt.Length]; + Array.Copy(psbytes, data00, psbytes.Length); //copy the pswd bytes + Array.Copy(salt, 0, data00, psbytes.Length, salt.Length); //concatenate the salt bytes + + // ---- do multi-hashing and concatenate results D1, D2 ... into keymaterial bytes ---- + MD5 md5 = new MD5CryptoServiceProvider(); + byte[]{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} result = null; + byte[] hashtarget = new byte[HASHLENGTH + data00.Length]; //fixed length initial hashtarget + + for (int j = 0; j < miter; j++) + { + // ---- Now hash consecutively for count times ------ + if (j == 0) + result = data00; //initialize + else + { + Array.Copy(result{{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}, hashtarget, result{{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}.Length); // TODO: what do we do if result is null here? + Array.Copy(data00, 0, hashtarget, result.Length, data00.Length); + result = hashtarget; + } + + for (int i = 0; i < count; i++) + result = md5.ComputeHash(result); + + Array.Copy(result, 0, keymaterial, j * HASHLENGTH, result.Length); //concatenate to keymaterial + } + byte[] deskey = new byte[24]; + Array.Copy(keymaterial, deskey, deskey.Length); + + Array.Clear(psbytes, 0, psbytes.Length); + Array.Clear(data00, 0, data00.Length); + Array.Clear(result{{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}, 0, result{{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}.Length); // TODO: what do we do if result is null here? + Array.Clear(hashtarget, 0, hashtarget.Length); + Array.Clear(keymaterial, 0, keymaterial.Length); + return deskey; + } + + private byte[]{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} DecryptKey(byte[] cipherData, byte[] desKey, byte[] IV) + { + MemoryStream memst = new MemoryStream(); + TripleDES alg = TripleDES.Create(); + alg.Key = desKey; + alg.IV = IV; + try + { + CryptoStream cs = new CryptoStream(memst, alg.CreateDecryptor(), CryptoStreamMode.Write); + cs.Write(cipherData, 0, cipherData.Length); + cs.Close(); + } + catch (Exception) + { + return null; + } + byte[] decryptedData = memst.ToArray(); + return decryptedData; + } + + /// + /// Detect the key type from the pem file. + /// + /// key file path in pem format + /// + private PrivateKeyType GetKeyType(string keyFilePath) + { + if (!File.Exists(keyFilePath)) + throw new Exception("Key file path does not exist."); + + var ecPrivateKeyHeader = "BEGIN EC PRIVATE KEY"; + var ecPrivateKeyFooter = "END EC PRIVATE KEY"; + var rsaPrivateKeyHeader = "BEGIN RSA PRIVATE KEY"; + var rsaPrivateFooter = "END RSA PRIVATE KEY"; + //var pkcs8Header = "BEGIN PRIVATE KEY"; + //var pkcs8Footer = "END PRIVATE KEY"; + var keyType = PrivateKeyType.None; + var key = File.ReadAllLines(keyFilePath); + + if (key[0].ToString().Contains(rsaPrivateKeyHeader) && key[key.Length - 1].ToString().Contains(rsaPrivateFooter)) + keyType = PrivateKeyType.RSA; + else if (key[0].ToString().Contains(ecPrivateKeyHeader) && key[key.Length - 1].ToString().Contains(ecPrivateKeyFooter)) + keyType = PrivateKeyType.ECDSA; + + else if (key[0].ToString().Contains(ecPrivateKeyHeader) && key[key.Length - 1].ToString().Contains(ecPrivateKeyFooter)) + { + /* this type of key can hold many type different types of private key, but here due lack of pem header + Considering this as EC key + */ + //TODO :- update the key based on oid + keyType = PrivateKeyType.ECDSA; + } + else + throw new Exception("Either the key is invalid or key is not supported"); + + return keyType; + } + #endregion + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningToken.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningToken.mustache new file mode 100644 index 000000000000..ca47cca24da3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/HttpSigningToken.mustache @@ -0,0 +1,43 @@ +// +{{partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace {{packageName}}.Client +{ + /// + /// A token constructed from an HttpSigningConfiguration + /// + public class HttpSignatureToken : TokenBase + { + private HttpSigningConfiguration _configuration; + + /// + /// Constructs an HttpSignatureToken object. + /// + /// + /// + public HttpSignatureToken(HttpSigningConfiguration configuration, TimeSpan? timeout = null) : base(timeout) + { + _configuration = configuration; + } + + /// + /// Places the token in the header. + /// + /// + /// + /// + public void UseInHeader(System.Net.Http.HttpRequestMessage request, string requestBody, CancellationToken? cancellationToken = null) + { + var signedHeaders = _configuration.GetHttpSignedHeader(request, requestBody, cancellationToken); + + foreach (var signedHeader in signedHeaders) + request.Headers.Add(signedHeader.Key, signedHeader.Value); + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/IApi.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/IApi.mustache new file mode 100644 index 000000000000..78856d47816c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/IApi.mustache @@ -0,0 +1,21 @@ +using System.Net.Http; + +namespace {{packageName}}.Client +{ + /// + /// Any Api client + /// + public interface {{interfacePrefix}}Api + { + /// + /// The HttpClient + /// + HttpClient HttpClient { get; } + + /// + /// An event to track the health of the server. + /// If you store these event args, be sure to purge old event args to prevent a memory leak. + /// + event ClientUtils.EventHandler{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ApiResponded; + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/OAuthToken.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/OAuthToken.mustache new file mode 100644 index 000000000000..d15a01cf9d3f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/OAuthToken.mustache @@ -0,0 +1,39 @@ +// +{{partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace {{packageName}}.Client +{ + /// + /// A token constructed with OAuth. + /// + public class OAuthToken : TokenBase + { + private string _raw; + + /// + /// Consturcts an OAuthToken object. + /// + /// + /// + public OAuthToken(string value, TimeSpan? timeout = null) : base(timeout) + { + _raw = value; + } + + /// + /// Places the token in the header. + /// + /// + /// + public virtual void UseInHeader(System.Net.Http.HttpRequestMessage request, string headerName) + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _raw); + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/README.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/README.mustache new file mode 100644 index 000000000000..608e3fa8654c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/README.mustache @@ -0,0 +1,214 @@ +# Created with Openapi Generator + + +## Run the following powershell command to generate the library + +```ps1 +$properties = @( + 'apiName={{apiName}}', + 'targetFramework={{targetFramework}}', + 'validatable={{validatable}}', + 'nullableReferenceTypes={{nullableReferenceTypes}}', + 'hideGenerationTimestamp={{hideGenerationTimestamp}}', + 'packageVersion={{packageVersion}}', + 'packageAuthors={{packageAuthors}}', + 'packageCompany={{packageCompany}}', + 'packageCopyright={{packageCopyright}}', + 'packageDescription={{packageDescription}}',{{#licenseId}} + 'licenseId={{.}}',{{/licenseId}} + 'packageName={{packageName}}', + 'packageTags={{packageTags}}', + 'packageTitle={{packageTitle}}' +) -join "," + +$global = @( + 'apiDocs={{generateApiDocs}}', + 'modelDocs={{generateModelDocs}}', + 'apiTests={{generateApiTests}}', + 'modelTests={{generateModelTests}}' +) -join "," + +java -jar "/openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar" generate ` + -g csharp-netcore ` + -i .yaml ` + -o ` + --library generichost ` + --additional-properties $properties ` + --global-property $global ` + --git-host "{{gitHost}}" ` + --git-repo-id "{{gitRepoId}}" ` + --git-user-id "{{gitUserId}}" ` + --release-note "{{releaseNote}}" + # -t templates +``` + + +## Using the library in your project + +```cs +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; +using {{packageName}}.Api; +using {{packageName}}.Client; +using {{packageName}}.Model; + +namespace YourProject +{ + public class Program + { + public static async Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build();{{#apiInfo}}{{#apis}}{{#-first}} + var api = host.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>();{{#operations}}{{#-first}}{{#operation}}{{#-first}} + ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> foo = await api.{{operationId}}WithHttpInfoAsync("todo");{{/-first}}{{/operation}}{{/-first}}{{/operations}}{{/-first}}{{/apis}}{{/apiInfo}} + } + + public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .Configure{{apiName}}((context, options) => + { + {{#authMethods}}// the type of token here depends on the api security specifications + ApiKeyToken token = new(""); + options.AddTokens(token); + + // optionally choose the method the tokens will be provided with, default is RateLimitProvider + options.UseProvider, ApiKeyToken>(); + + {{/authMethods}}options.ConfigureJsonOptions((jsonOptions) => + { + // your custom converters if any + }); + + options.Add{{apiName}}HttpClients(builder: builder => builder + .AddRetryPolicy(2) + .AddTimeoutPolicy(TimeSpan.FromSeconds(5)) + .AddCircuitBreakerPolicy(10, TimeSpan.FromSeconds(30)) + // add whatever middleware you prefer + ); + }); + } +} +``` + +## Questions + +- What about HttpRequest failures and retries? + If supportsRetry is enabled, you can configure Polly in the ConfigureClients method. +- How are tokens used? + Tokens are provided by a TokenProvider class. The default is RateLimitProvider which will perform client side rate limiting. + Other providers can be used with the UseProvider method. +- Does an HttpRequest throw an error when the server response is not Ok? + It depends how you made the request. If the return type is ApiResponse no error will be thrown, though the Content property will be null. + StatusCode and ReasonPhrase will contain information about the error. + If the return type is T, then it will throw. If the return type is TOrDefault, it will return null. + + +## Dependencies + +- [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting/) - 5.0.0 or later +- [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http/) - 5.0.0 or later{{#supportsRetry}} +- [Microsoft.Extensions.Http.Polly](https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly/) - 5.0.1 or later +- [Polly](https://www.nuget.org/packages/Polly/) - 7.2.2 or later{{/supportsRetry}} +- [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json/) - 12.0.3 or later +- [JsonSubTypes](https://www.nuget.org/packages/JsonSubTypes/) - 1.7.0 or later{{#useCompareNetObjects}} +- [CompareNETObjects](https://www.nuget.org/packages/CompareNETObjects) - 4.61.0 or later{{/useCompareNetObjects}}{{#validatable}} +- [System.ComponentModel.Annotations](https://www.nuget.org/packages/System.ComponentModel.Annotations) - 4.7.0 or later{{/validatable}}{{#apiDocs}} + + +## Documentation for API Endpoints + +All URIs are relative to *{{{basePath}}}* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | -------------{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} +*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{{summary}}}{{/summary}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}{{/apiDocs}}{{#modelDocs}} + + +## Documentation for Models + +{{#modelPackage}}{{#models}}{{#model}} - [{{{modelPackage}}}.{{{classname}}}]({{modelDocPath}}{{{classname}}}.md){{/model}}{{/models}}{{/modelPackage}} +{{^modelPackage}}No model defined in this package{{/modelPackage}}{{/modelDocs}} + + +## Documentation for Authorization + +{{^authMethods}}All endpoints do not require authorization.{{/authMethods}}{{#authMethods}}{{#-last}}Authentication schemes defined for the API:{{/-last}}{{/authMethods}}{{#authMethods}} + + +### {{name}} + +{{#isApiKey}}- **Type**: API key +- **API key parameter name**: {{keyParamName}} +- **Location**: {{#isKeyInQuery}}URL query string{{/isKeyInQuery}}{{#isKeyInHeader}}HTTP header{{/isKeyInHeader}}{{/isApiKey}}{{#isBasicBasic}} +- **Type**: HTTP basic authentication{{/isBasicBasic}}{{#isBasicBearer}} +- **Type**: Bearer Authentication{{/isBasicBearer}}{{#isOAuth}} +- **Type**: OAuth +- **Flow**: {{flow}} +- **Authorization URL**: {{authorizationUrl}} +- **Scopes**: {{^scopes}}N/A{{/scopes}}{{#scopes}} +- {{scope}}: {{description}}{{/scopes}}{{/isOAuth}}{{/authMethods}} + +## Build +- SDK version: {{packageVersion}}{{^hideGenerationTimestamp}} +- Build date: {{generatedDate}}{{/hideGenerationTimestamp}} +- Build package: {{generatorClass}} + +## Api Information +- appName: {{appName}} +- appVersion: {{appVersion}} +- appDescription: {{appDescription}} + +## [OpenApi Global properties](https://openapi-generator.tech/docs/globals) +- generateAliasAsModel: {{generateAliasAsModel}} +- supportingFiles: {{supportingFiles}} +- models: omitted for brevity +- apis: omitted for brevity +- apiDocs: {{generateApiDocs}} +- modelDocs: {{generateModelDocs}} +- apiTests: {{generateApiTests}} +- modelTests: {{generateModelTests}} +- withXml: {{withXml}} + +## [OpenApi Generator Parameteres](https://openapi-generator.tech/docs/generators/csharp-netcore) +- allowUnicodeIdentifiers: {{allowUnicodeIdentifiers}} +- apiName: {{apiName}} +- caseInsensitiveResponseHeaders: {{caseInsensitiveResponseHeaders}} +- conditionalSerialization: {{conditionalSerialization}} +- disallowAdditionalPropertiesIfNotPresent: {{disallowAdditionalPropertiesIfNotPresent}} +- gitHost: {{gitHost}} +- gitRepoId: {{gitRepoId}} +- gitUserId: {{gitUserId}} +- hideGenerationTimestamp: {{hideGenerationTimestamp}} +- interfacePrefix: {{interfacePrefix}} +- library: {{library}} +- licenseId: {{licenseId}} +- modelPropertyNaming: {{modelPropertyNaming}} +- netCoreProjectFile: {{netCoreProjectFile}} +- nonPublicApi: {{nonPublicApi}} +- nullableReferenceTypes: {{nullableReferenceTypes}} +- optionalAssemblyInfo: {{optionalAssemblyInfo}} +- optionalEmitDefaultValues: {{optionalEmitDefaultValues}} +- optionalMethodArgument: {{optionalMethodArgument}} +- optionalProjectFile: {{optionalProjectFile}} +- packageAuthors: {{packageAuthors}} +- packageCompany: {{packageCompany}} +- packageCopyright: {{packageCopyright}} +- packageDescription: {{packageDescription}} +- packageGuid: {{packageGuid}} +- packageName: {{packageName}} +- packageTags: {{packageTags}} +- packageTitle: {{packageTitle}} +- packageVersion: {{packageVersion}} +- releaseNote: {{releaseNote}} +- returnICollection: {{returnICollection}} +- sortParamsByRequiredFlag: {{sortParamsByRequiredFlag}} +- sourceFolder: {{sourceFolder}} +- targetFramework: {{targetFramework}} +- useCollection: {{useCollection}} +- useDateTimeOffset: {{useDateTimeOffset}} +- useOneOfDiscriminatorLookup: {{useOneOfDiscriminatorLookup}} +- validatable: {{validatable}}{{#infoUrl}} +For more information, please visit [{{{.}}}]({{{.}}}){{/infoUrl}} + +This C# SDK is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project. \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/RateLimitProvider`1.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/RateLimitProvider`1.mustache new file mode 100644 index 000000000000..3a778c0aeec9 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/RateLimitProvider`1.mustache @@ -0,0 +1,106 @@ +// +{{>partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System;{{^netStandard}} +using System.Threading.Channels;{{/netStandard}}{{#netStandard}} +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks;{{/netStandard}} + +namespace {{packageName}}.Client {{^netStandard}} +{ + /// + /// Provides a token to the api clients. Tokens will be rate limited based on the provided TimeSpan. + /// + /// + public class RateLimitProvider : TokenProvider where TTokenBase : TokenBase + { + internal Channel AvailableTokens { get; } + + /// + /// Instantiates a ThrottledTokenProvider. Your tokens will be rate limited based on the token's timeout. + /// + /// + public RateLimitProvider(TokenContainer container) : base(container.Tokens) + { + foreach(TTokenBase token in _tokens) + token.StartTimer(token.Timeout ?? TimeSpan.FromMilliseconds(40)); + + BoundedChannelOptions options = new BoundedChannelOptions(_tokens.Length) + { + FullMode = BoundedChannelFullMode.DropWrite + }; + + AvailableTokens = Channel.CreateBounded(options); + + for (int i = 0; i < _tokens.Length; i++) + _tokens[i].TokenBecameAvailable += ((sender) => AvailableTokens.Writer.TryWrite((TTokenBase) sender)); + } + + internal override async System.Threading.Tasks.ValueTask GetAsync(System.Threading.CancellationToken? cancellation = null) + => await AvailableTokens.Reader.ReadAsync(cancellation.GetValueOrDefault()).ConfigureAwait(false); + } +} {{/netStandard}}{{#netStandard}} +{ + /// + /// Provides a token to the api clients. Tokens will be rate limited based on the provided TimeSpan. + /// + /// + public class RateLimitProvider : TokenProvider where TTokenBase : TokenBase + { + internal ConcurrentDictionary AvailableTokens = new ConcurrentDictionary(); + private SemaphoreSlim _semaphore; + + /// + /// Instantiates a ThrottledTokenProvider. Your tokens will be rate limited based on the token's timeout. + /// + /// + public RateLimitProvider(TokenContainer container) : base(container.Tokens) + { + _semaphore = new SemaphoreSlim(1, 1); + + foreach(TTokenBase token in _tokens) + token.StartTimer(token.Timeout ?? TimeSpan.FromMilliseconds(40)); + + for (int i = 0; i < _tokens.Length; i++) + { + _tokens[i].TokenBecameAvailable += ((sender) => + { + TTokenBase token = (TTokenBase)sender; + + AvailableTokens.TryAdd(token, token); + }); + } + } + + internal override async System.Threading.Tasks.ValueTask GetAsync(System.Threading.CancellationToken? cancellation = null) + { + await _semaphore.WaitAsync().ConfigureAwait(false); + + try + { + TTokenBase result = null; + + while (result == null) + { + TTokenBase tokenToRemove = AvailableTokens.FirstOrDefault().Value; + + if (tokenToRemove != null && AvailableTokens.TryRemove(tokenToRemove, out result)) + return result; + + await Task.Delay(40).ConfigureAwait(false); + + tokenToRemove = AvailableTokens.FirstOrDefault().Value; + } + + return result; + } + finally + { + _semaphore.Release(); + } + } + } +} {{/netStandard}} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenBase.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenBase.mustache new file mode 100644 index 000000000000..0b8db5d10ae1 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenBase.mustache @@ -0,0 +1,71 @@ +// +{{partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; + +namespace {{packageName}}.Client +{ + /// + /// The base for all tokens. + /// + public abstract class TokenBase + { + private DateTime _nextAvailable = DateTime.UtcNow; + private object _nextAvailableLock = new object(); + private readonly System.Timers.Timer _timer = new System.Timers.Timer(); + + + internal TimeSpan? Timeout { get; set; } + internal delegate void TokenBecameAvailableEventHandler(object sender); + internal event TokenBecameAvailableEventHandler{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} TokenBecameAvailable; + + + /// + /// Initialize a TokenBase object. + /// + /// + internal TokenBase(TimeSpan? timeout = null) + { + Timeout = timeout; + + if (Timeout != null) + StartTimer(Timeout.Value); + } + + + /// + /// Starts the token's timer + /// + /// + internal void StartTimer(TimeSpan timeout) + { + Timeout = timeout; + _timer.Interval = Timeout.Value.TotalMilliseconds; + _timer.Elapsed += OnTimer; + _timer.AutoReset = true; + _timer.Start(); + } + + /// + /// Returns true while the token is rate limited. + /// + public bool IsRateLimited => _nextAvailable > DateTime.UtcNow; + + /// + /// Triggered when the server returns status code TooManyRequests + /// Once triggered the local timeout will be extended an arbitrary length of time. + /// + public void BeginRateLimit() + { + lock(_nextAvailableLock) + _nextAvailable = DateTime.UtcNow.AddSeconds(5); + } + + private void OnTimer(object sender, System.Timers.ElapsedEventArgs e) + { + if (TokenBecameAvailable != null && !IsRateLimited) + TokenBecameAvailable.Invoke(this); + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenContainer`1.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenContainer`1.mustache new file mode 100644 index 000000000000..113f9206fd19 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenContainer`1.mustache @@ -0,0 +1,37 @@ +// +{{partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System.Linq; +using System.Collections.Generic; + +namespace {{packageName}}.Client +{ + /// + /// A container for a collection of tokens. + /// + /// + public sealed class TokenContainer where TTokenBase : TokenBase + { + /// + /// The collection of tokens + /// + public List Tokens { get; } = new List(); + + /// + /// Instantiates a TokenContainer + /// + public TokenContainer() + { + } + + /// + /// Instantiates a TokenContainer + /// + /// + public TokenContainer(System.Collections.Generic.IEnumerable tokens) + { + Tokens = tokens.ToList(); + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenProvider`1.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenProvider`1.mustache new file mode 100644 index 000000000000..d8b467a30bf7 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/TokenProvider`1.mustache @@ -0,0 +1,36 @@ +// +{{>partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Linq; +using System.Collections.Generic; +using {{packageName}}.Client; + +namespace {{packageName}} +{ + /// + /// A class which will provide tokens. + /// + public abstract class TokenProvider where TTokenBase : TokenBase + { + /// + /// The array of tokens. + /// + protected TTokenBase[] _tokens; + + internal abstract System.Threading.Tasks.ValueTask GetAsync(System.Threading.CancellationToken? cancellation = null); + + /// + /// Instantiates a TokenProvider. + /// + /// + public TokenProvider(IEnumerable tokens) + { + _tokens = tokens.ToArray(); + + if (_tokens.Length == 0) + throw new ArgumentException("You did not provide any tokens."); + } + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api.mustache new file mode 100644 index 000000000000..880952325e61 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api.mustache @@ -0,0 +1,391 @@ +// +{{>partial_header}} +{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}} + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using System.Net.Http; +using System.Net.Http.Headers; +using {{packageName}}.Client; +{{#hasImport}} +using {{packageName}}.{{modelPackage}}; +{{/hasImport}} + +namespace {{packageName}}.{{apiPackage}} +{ + {{#operations}} + /// + /// Represents a collection of functions to interact with the API endpoints + /// + {{>visibility}} interface {{interfacePrefix}}{{classname}} : IApi + { + {{#operation}} + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// Task<ApiResponse<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>> + Task> {{operationId}}WithHttpInfoAsync({{#allParams}}{{#required}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/required}}{{^required}}{{{dataType}}} {{paramName}} = null{{^-last}}, {{/-last}}{{/required}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken? cancellationToken = null); + + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// Task of ApiResponse<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}> + Task<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> {{operationId}}Async({{#allParams}}{{#required}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/required}}{{^required}}{{{dataType}}} {{paramName}} = null{{^-last}}, {{/-last}}{{/required}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken? cancellationToken = null);{{#nullableReferenceTypes}} + + /// + /// {{summary}} + /// + /// + /// {{notes}} + /// + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// Task of ApiResponse<{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}?> + Task<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> {{operationId}}OrDefaultAsync({{#allParams}}{{#required}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/required}}{{^required}}{{{dataType}}} {{paramName}} = null{{^-last}}, {{/-last}}{{/required}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken? cancellationToken = null);{{/nullableReferenceTypes}}{{^-last}} + + {{/-last}}{{/operation}} + } + + /// + /// Represents a collection of functions to interact with the API endpoints + /// + {{>visibility}} partial class {{classname}} : {{interfacePrefix}}{{classname}} + { + /// + /// An event to track the health of the server. + /// If you store these event args, be sure to purge old event args to prevent a memory leak. + /// + public event ClientUtils.EventHandler{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ApiResponded; + + /// + /// The logger + /// + public ILogger<{{classname}}> Logger { get; } + + /// + /// The HttpClient + /// + public HttpClient HttpClient { get; }{{#hasApiKeyMethods}} + + /// + /// A token provider of type + /// + public TokenProvider ApiKeyProvider { get; }{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}} + + /// + /// A token provider of type + /// + public TokenProvider BearerTokenProvider { get; }{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}} + + /// + /// A token provider of type + /// + public TokenProvider BasicTokenProvider { get; }{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}} + + /// + /// A token provider of type + /// + public TokenProvider HttpSignatureTokenProvider { get; }{{/hasHttpSignatureMethods}}{{#hasOAuthMethods}} + + /// + /// A token provider of type + /// + public TokenProvider OauthTokenProvider { get; }{{/hasOAuthMethods}} + + /// + /// Initializes a new instance of the class. + /// + /// + public {{classname}}(ILogger<{{classname}}> logger, HttpClient httpClient{{#hasApiKeyMethods}}, + TokenProvider apiKeyProvider{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}}, + TokenProvider bearerTokenProvider{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}}, + TokenProvider basicTokenProvider{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}}, + TokenProvider httpSignatureTokenProvider{{/hasHttpSignatureMethods}}{{#hasOAuthMethods}}, + TokenProvider oauthTokenProvider{{/hasOAuthMethods}}) + { + Logger = logger; + HttpClient = httpClient;{{#hasApiKeyMethods}} + ApiKeyProvider = apiKeyProvider;{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}} + BearerTokenProvider = bearerTokenProvider;{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}} + BasicTokenProvider = basicTokenProvider;{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}} + HttpSignatureTokenProvider = httpSignatureTokenProvider;{{/hasHttpSignatureMethods}}{{#hasOAuthMethods}} + OauthTokenProvider = oauthTokenProvider;{{/hasOAuthMethods}} + } + {{#operation}} + + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// <> + public async Task<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> {{operationId}}Async({{#allParams}}{{#required}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/required}}{{^required}}{{{dataType}}} {{paramName}} = null{{^-last}}, {{/-last}}{{/required}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken? cancellationToken = null) + { + ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> result = await {{operationId}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false); + + {{^nullableReferenceTypes}}{{#returnTypeIsPrimitive}}#pragma warning disable CS0472 // The result of the expression is always the same since a value of this type is never equal to 'null' + {{/returnTypeIsPrimitive}}{{/nullableReferenceTypes}}if (result.Content == null){{^nullableReferenceTypes}}{{#returnTypeIsPrimitive}} + #pragma warning disable CS0472 // The result of the expression is always the same since a value of this type is never equal to 'null'{{/returnTypeIsPrimitive}}{{/nullableReferenceTypes}} + throw new ApiException(result.ReasonPhrase, result.StatusCode, result.RawContent); + + return result.Content; + }{{#nullableReferenceTypes}} + + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// <> + public async Task<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> {{operationId}}OrDefaultAsync({{#allParams}}{{#required}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/required}}{{^required}}{{{dataType}}} {{paramName}} = null{{^-last}}, {{/-last}}{{/required}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken? cancellationToken = null) + { + ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} result = null; + try + { + result = await {{operationId}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + } + + return result != null && result.IsSuccessStatusCode + ? result.Content + : null; + }{{/nullableReferenceTypes}}{{^nullableReferenceTypes}}{{^returnTypeIsPrimitive}} +{{! Note that this method is a copy paste of above due to NRT complexities }} + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// <> + public async Task<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> {{operationId}}OrDefaultAsync({{#allParams}}{{#required}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/required}}{{^required}}{{{dataType}}} {{paramName}} = null{{^-last}}, {{/-last}}{{/required}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken? cancellationToken = null) + { + ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} result = null; + try + { + result = await {{operationId}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + } + + return result != null && result.IsSuccessStatusCode + ? result.Content + : null; + } + {{/returnTypeIsPrimitive}}{{/nullableReferenceTypes}} + + /// + /// {{summary}} {{notes}} + /// + /// Thrown when fails to make API call + {{#allParams}} + /// {{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + /// Cancellation Token to cancel the request. + /// <> where T : + public async Task> {{operationId}}WithHttpInfoAsync({{#allParams}}{{#required}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/required}}{{^required}}{{{dataType}}} {{paramName}} = null{{^-last}}, {{/-last}}{{/required}}{{/allParams}}{{#allParams.0}}, {{/allParams.0}}System.Threading.CancellationToken? cancellationToken = null) + { + try + { + {{#hasRequiredParams}}#pragma warning disable CS0472 // The result of the expression is always the same since a value of this type is never equal to 'null'{{/hasRequiredParams}}{{#allParams}}{{#required}}{{#nullableReferenceTypes}} + + if ({{paramName}} == null) + throw new ArgumentNullException(nameof({{paramName}}));{{/nullableReferenceTypes}}{{^nullableReferenceTypes}}{{^vendorExtensions.x-csharp-value-type}} + + if ({{paramName}} == null) + throw new ArgumentNullException(nameof({{paramName}}));{{/vendorExtensions.x-csharp-value-type}}{{/nullableReferenceTypes}}{{/required}}{{/allParams}}{{#hasRequiredParams}} + + #pragma warning disable CS0472 // The result of the expression is always the same since a value of this type is never equal to 'null' + + {{/hasRequiredParams}}using (HttpRequestMessage request = new HttpRequestMessage()) + { + UriBuilder uriBuilder = new UriBuilder(); + uriBuilder.Host = HttpClient.BaseAddress{{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}.Host; + uriBuilder.Scheme = ClientUtils.SCHEME; + uriBuilder.Path = ClientUtils.CONTEXT_PATH + "{{path}}";{{#pathParams}}{{#required}} + uriBuilder.Path = uriBuilder.Path.Replace("%7B{{baseName}}%7D", Uri.EscapeDataString({{paramName}}.ToString()));{{/required}}{{^required}} + + if ({{paramName}} != null) + uriBuilder.Path = uriBuilder.Path + $"/{ Uri.EscapeDataString({{paramName}}).ToString()) }"; + {{/required}}{{/pathParams}}{{#queryParams}}{{#-first}} + + System.Collections.Specialized.NameValueCollection parseQueryString = System.Web.HttpUtility.ParseQueryString(string.Empty);{{/-first}}{{/queryParams}}{{^queryParams}}{{#authMethods}}{{#isApiKey}}{{#isKeyInQuery}} + + System.Collections.Specialized.NameValueCollection parseQueryString = System.Web.HttpUtility.ParseQueryString(string.Empty);{{/isKeyInQuery}}{{/isApiKey}}{{/authMethods}}{{/queryParams}}{{#queryParams}}{{#required}}{{#-first}} + + {{! all the redundant tags here are to get the spacing just right }} + {{/-first}}{{/required}}{{/queryParams}}{{#queryParams}}{{#required}}parseQueryString["{{baseName}}"] = Uri.EscapeDataString({{paramName}}.ToString(){{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}); + {{/required}}{{/queryParams}}{{#queryParams}}{{#-first}} + {{/-first}}{{/queryParams}}{{#queryParams}}{{^required}}if ({{paramName}} != null) + parseQueryString["{{baseName}}"] = Uri.EscapeDataString({{paramName}}.ToString(){{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}); + + {{/required}}{{#-last}}uriBuilder.Query = parseQueryString.ToString();{{/-last}}{{/queryParams}}{{#headerParams}}{{#required}} + + request.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}));{{/required}}{{^required}} + + if ({{paramName}} != null) + request.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}));{{/required}}{{/headerParams}}{{#formParams}}{{#-first}} + + MultipartContent multipartContent = new MultipartContent(); + + request.Content = multipartContent; + + List> formParams = new List>(); + + multipartContent.Add(new FormUrlEncodedContent(formParams));{{/-first}}{{^isFile}}{{#required}} + + formParams.Add(new KeyValuePair("{{baseName}}", ClientUtils.ParameterToString({{paramName}})));{{/required}}{{^required}} + + if ({{paramName}} != null) + formParams.Add(new KeyValuePair("{{baseName}}", ClientUtils.ParameterToString({{paramName}})));{{/required}}{{/isFile}}{{#isFile}}{{#required}} + + multipartContent.Add(new StreamContent({{paramName}}));{{/required}}{{^required}} + + if ({{paramName}} != null) + multipartContent.Add(new StreamContent({{paramName}}));{{/required}}{{/isFile}}{{/formParams}}{{#bodyParam}} + + if (({{paramName}} as object) is System.IO.Stream stream) + request.Content = new StreamContent(stream); + else + request.Content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject({{paramName}}, ClientUtils.JsonSerializerSettings));{{/bodyParam}}{{#authMethods}}{{#-first}} + + List tokens = new List();{{/-first}}{{#isApiKey}} + + ApiKeyToken apiKey = (ApiKeyToken) await ApiKeyProvider.GetAsync(cancellationToken).ConfigureAwait(false); + + tokens.Add(apiKey);{{#isKeyInHeader}} + + apiKey.UseInHeader(request, "{{keyParamName}}");{{/isKeyInHeader}}{{#isKeyInQuery}} + + apiKey.UseInQuery(request, uriBuilder, parseQueryString, "{{keyParamName}}"); + + uriBuilder.Query = parseQueryString.ToString();{{/isKeyInQuery}}{{#isKeyInCookie}} + + apiKey.UseInCookie(request, parseQueryString, "{{keyParamName}}"); + + uriBuilder.Query = parseQueryString.ToString();{{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}} + + {{! below line must be after any UseInQuery calls, but before using the HttpSignatureToken}} + request.RequestUri = uriBuilder.Uri;{{#authMethods}}{{#isBasicBasic}} + + BasicToken basicToken = (BasicToken) await BasicTokenProvider.GetAsync(cancellationToken).ConfigureAwait(false); + + tokens.Add(basicToken); + + basicToken.UseInHeader(request, "{{keyParamName}}");{{/isBasicBasic}}{{#isBasicBearer}} + + BearerToken bearerToken = (BearerToken) await BearerTokenProvider.GetAsync(cancellationToken).ConfigureAwait(false); + + tokens.Add(bearerToken); + + bearerToken.UseInHeader(request, "{{keyParamName}}");{{/isBasicBearer}}{{#isOAuth}} + + OAuthToken oauthToken = (OAuthToken) await OauthTokenProvider.GetAsync(cancellationToken).ConfigureAwait(false); + + tokens.Add(oauthToken); + + oauthToken.UseInHeader(request, "{{keyParamName}}");{{/isOAuth}}{{#isHttpSignature}} + + HttpSignatureToken signatureToken = (HttpSignatureToken) await HttpSignatureTokenProvider.GetAsync(cancellationToken).ConfigureAwait(false); + + tokens.Add(signatureToken); + + string requestBody = await request.Content{{#nullableReferenceTypes}}!{{/nullableReferenceTypes}}.ReadAsStringAsync({{^netStandard}}{{^netcoreapp3.1}}cancellationToken.GetValueOrDefault(){{/netcoreapp3.1}}{{/netStandard}}).ConfigureAwait(false); + + signatureToken.UseInHeader(request, requestBody, cancellationToken);{{/isHttpSignature}}{{/authMethods}}{{#consumes}}{{#-first}} + + string[] contentTypes = new string[] { + {{/-first}}"{{{mediaType}}}"{{^-last}}, + {{/-last}}{{#-last}} + };{{/-last}}{{/consumes}}{{#consumes}}{{#-first}} + + string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} contentType = ClientUtils.SelectHeaderContentType(contentTypes); + + if (contentType != null) + request.Content.Headers.Add("ContentType", contentType);{{/-first}}{{/consumes}}{{#produces}}{{#-first}} + + string[] accepts = new string[] { {{/-first}}{{/produces}} + {{#produces}}"{{{mediaType}}}"{{^-last}}, + {{/-last}}{{/produces}}{{#produces}}{{#-last}} + };{{/-last}}{{/produces}}{{#produces}}{{#-first}} + + string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} accept = ClientUtils.SelectHeaderAccept(accepts); + + if (accept != null) + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(accept)); + {{/-first}}{{/produces}}{{^netStandard}} + request.Method = HttpMethod.{{#lambda.titlecase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.titlecase}};{{/netStandard}}{{#netStandard}} + request.Method = new HttpMethod("{{#lambda.uppercase}}{{httpMethod}}{{/lambda.uppercase}}");{{/netStandard}} {{! early standard versions do not have HttpMethod.Patch }} + + using (HttpResponseMessage responseMessage = await HttpClient.SendAsync(request, cancellationToken.GetValueOrDefault()).ConfigureAwait(false)) + { + DateTime requestedAt = DateTime.UtcNow; + + string responseContent = await responseMessage.Content.ReadAsStringAsync({{^netStandard}}{{^netcoreapp3.1}}cancellationToken.GetValueOrDefault(){{/netcoreapp3.1}}{{/netStandard}}).ConfigureAwait(false); + + if (ApiResponded != null) + { + try + { + ApiResponded.Invoke(this, new ApiResponseEventArgs(requestedAt, DateTime.UtcNow, responseMessage.StatusCode, "{{path}}")); + } + catch(Exception e) + { + Logger.LogError(e, "An error occured while invoking ApiResponded."); + } + } + + ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}> apiResponse = new ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>(responseMessage, responseContent); + + if (apiResponse.IsSuccessStatusCode) + apiResponse.Content = Newtonsoft.Json.JsonConvert.DeserializeObject<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}>(apiResponse.RawContent, ClientUtils.JsonSerializerSettings);{{#authMethods}} + else if (apiResponse.StatusCode == (HttpStatusCode) 429) + foreach(TokenBase token in tokens) + token.BeginRateLimit();{{/authMethods}} + + return apiResponse; + } + } + } + catch(Exception e) + { + Logger.LogError(e, "An error occured while sending the request to the server."); + throw; + } + } + {{/operation}} + } + {{/operations}} +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api_test.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api_test.mustache new file mode 100644 index 000000000000..b64731f81dd4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/api_test.mustache @@ -0,0 +1,46 @@ +{{>partial_header}} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using Microsoft.Extensions.DependencyInjection; +using {{packageName}}.{{apiPackage}};{{#hasImport}} +using {{packageName}}.{{modelPackage}};{{/hasImport}} + + +{{{testInstructions}}} + + +namespace {{packageName}}.Test.Api +{ + /// + /// Class for testing {{classname}} + /// + public sealed class {{classname}}Tests : ApiTestsBase + { + private readonly {{interfacePrefix}}{{classname}} _instance; + + public {{classname}}Tests(): base(Array.Empty()) + { + _instance = _host.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>(); + } + + {{#operations}} + {{#operation}} + + /// + /// Test {{operationId}} + /// + [Fact (Skip = "not implemented")] + public async Task {{operationId}}AsyncTest() + { + {{#allParams}} + {{{dataType}}} {{paramName}} = default; + {{/allParams}} + {{#returnType}}var response = {{/returnType}}await _instance.{{operationId}}Async({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});{{#returnType}} + Assert.IsType<{{{.}}}>(response);{{/returnType}} + } + {{/operation}} + {{/operations}} + } +} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.ps1.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.ps1.mustache new file mode 100644 index 000000000000..0f4084ef8179 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.ps1.mustache @@ -0,0 +1,75 @@ +param( + [Parameter()][Alias("g")][String]$GitHost = "{{{gitHost}}}", + [Parameter()][Alias("u")][String]$GitUserId = "{{{gitUserId}}}", + [Parameter()][Alias("r")][String]$GitRepoId = "{{{gitRepoId}}}", + [Parameter()][Alias("m")][string]$Message = "{{{releaseNote}}}", + [Parameter()][Alias("h")][switch]$Help +) + +function Publish-ToGitHost{ + if ([string]::IsNullOrWhiteSpace($Message) -or $Message -eq "Minor update"){ + # it seems unlikely that we would want our git commit message to be the default, so lets prompt the user + $Message = Read-Host -Prompt "Please provide a commit message or press enter" + $Message = if([string]::IsNullOrWhiteSpace($Message)) { "no message provided" } else { $Message } + } + + git init + git add . + git commit -am "${Message}" + $branchName=$(git rev-parse --abbrev-ref HEAD) + $gitRemote=$(git remote) + + if([string]::IsNullOrWhiteSpace($gitRemote)){ + git remote add origin https://${GitHost}/${GitUserId}/${GitRepoId}.git + } + + Write-Output "Pulling from https://${GitHost}/${GitUserId}/${GitRepoId}.git" + git pull origin $branchName --ff-only + + if ($LastExitCode -ne 0){ + if (${GitHost} -eq "github.com"){ + Write-Output "The ${GitRepoId} repository may not exist yet. Creating it now with the GitHub CLI." + gh auth login --hostname github.com --web + gh repo create $GitRepoId --private + # sleep 2 seconds to ensure git finishes creation of the repo + Start-Sleep -Seconds 2 + } + else{ + throw "There was an issue pulling the origin branch. The remote repository may not exist yet." + } + } + + Write-Output "Pushing to https://${GitHost}/${GitUserId}/${GitRepoId}.git" + git push origin $branchName +} + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 3.0 + +if ($Help){ + Write-Output " + This script will initialize a git repository, then add and commit all files. + The local repository will then be pushed to your prefered git provider. + If the remote repository does not exist yet and you are using GitHub, + the repository will be created for you provided you have the GitHub CLI installed. + + Parameters: + -g | -GitHost -> ex: github.com + -m | -Message -> the git commit message + -r | -GitRepoId -> the name of the repository + -u | -GitUserId -> your user id + " + + return +} + +$rootPath=Resolve-Path -Path $PSScriptRoot/../.. + +Push-Location $rootPath + +try { + Publish-ToGitHost $GitHost $GitUserId $GitRepoId $Message +} +finally{ + Pop-Location +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.sh.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.sh.mustache new file mode 100644 index 000000000000..3d4b710dc039 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/libraries/generichost/git_push.sh.mustache @@ -0,0 +1,49 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=${1:-{{{gitUserId}}}} +git_repo_id=${2:-{{{gitRepoId}}}} +release_note=${3:-{{{releaseNote}}}} +git_host=${4:-{{{gitHost}}}} + +starting_directory=$(pwd) +script_root="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" +cd $script_root +cd ../.. + +if [ "$release_note" = "" ] || [ "$release_note" = "Minor update" ]; then + # it seems unlikely that we would want our git commit message to be the default, so lets prompt the user + echo "Please provide a commit message or press enter" + read user_input + release_note=$user_input + if [ "$release_note" = "" ]; then + release_note="no message provided" + fi +fi + +git init +git add . +git commit -am "$release_note" +branch_name=$(git rev-parse --abbrev-ref HEAD) +git_remote=$(git remote) + +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +echo "[INFO] Pulling from https://${git_host}/${git_user_id}/${git_repo_id}.git" +git pull origin $branch_name --ff-only + +echo "[INFO] Pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin $branch_name + +cd $starting_directory diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/modelAnyOf.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/modelAnyOf.mustache index 7c3b0cc2d94a..f216a9911309 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/modelAnyOf.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/modelAnyOf.mustache @@ -171,6 +171,7 @@ } } + {{#validatable}} /// /// To validate all properties of the instance /// @@ -180,6 +181,7 @@ { yield break; } + {{/validatable}} } /// diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/modelOneOf.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/modelOneOf.mustache index d99b82a1e9e6..f60d3cf10f38 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/modelOneOf.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/modelOneOf.mustache @@ -216,6 +216,7 @@ } } + {{#validatable}} /// /// To validate all properties of the instance /// @@ -225,6 +226,7 @@ { yield break; } + {{/validatable}} } /// diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/model_doc.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/model_doc.mustache index babf25481c4e..3c7f8b2db810 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/model_doc.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/model_doc.mustache @@ -16,7 +16,7 @@ Name | Type | Description | Notes {{#vars}}**{{name}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{dataType}}**]({{complexType}}.md){{/isPrimitiveType}} | {{description}} | {{^required}}[optional] {{/required}}{{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}}[default to {{{.}}}]{{/defaultValue}} {{/vars}} -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) +[[Back to Model list]](../{{#useGenericHost}}../{{/useGenericHost}}README.md#documentation-for-models) [[Back to API list]](../{{#useGenericHost}}../{{/useGenericHost}}README.md#documentation-for-api-endpoints) [[Back to README]](../{{#useGenericHost}}../{{/useGenericHost}}README.md) {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_project.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_project.mustache index cdb95110a9e8..1a544ad8bb1e 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_project.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_project.mustache @@ -1,7 +1,8 @@ - - false + {{#useGenericHost}} + true {{/useGenericHost}}{{^useGenericHost}} + false{{/useGenericHost}} {{targetFramework}} {{packageName}} {{packageName}} @@ -33,6 +34,13 @@ {{#useRestSharp}} {{/useRestSharp}} + {{#useGenericHost}} + + + {{#supportsRetry}} + + {{/supportsRetry}} + {{/useGenericHost}} {{#supportsRetry}} {{/supportsRetry}} diff --git a/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_testproject.mustache b/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_testproject.mustache index b20a94c76c1a..d01627884333 100644 --- a/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_testproject.mustache +++ b/modules/openapi-generator/src/main/resources/csharp-netcore/netcore_testproject.mustache @@ -4,7 +4,8 @@ {{testPackageName}} {{testPackageName}} {{testTargetFramework}} - false + false{{#nullableReferenceTypes}} + annotations{{/nullableReferenceTypes}} diff --git a/samples/client/others/csharp-netcore-complex-files/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/others/csharp-netcore-complex-files/src/Org.OpenAPITools/Org.OpenAPITools.csproj index c445b1abb7cb..ea3a254a4d22 100644 --- a/samples/client/others/csharp-netcore-complex-files/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/others/csharp-netcore-complex-files/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false netstandard2.0 Org.OpenAPITools Org.OpenAPITools diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient-ConditionalSerialization/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/petstore/csharp-netcore/OpenAPIClient-ConditionalSerialization/src/Org.OpenAPITools/Org.OpenAPITools.csproj index c445b1abb7cb..ea3a254a4d22 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient-ConditionalSerialization/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient-ConditionalSerialization/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false netstandard2.0 Org.OpenAPITools Org.OpenAPITools diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Org.OpenAPITools.csproj index 0b425a6b53a1..2d4de6e7cccf 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient-httpclient/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false netstandard2.0 Org.OpenAPITools Org.OpenAPITools diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Org.OpenAPITools.csproj index 74dc32216c87..5e0f256b4cc5 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient-net47/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false net47 Org.OpenAPITools Org.OpenAPITools diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Org.OpenAPITools.csproj index d2fb8de69812..40cff9b3c47c 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient-net5.0/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false net5.0 Org.OpenAPITools Org.OpenAPITools diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Org.OpenAPITools.csproj index c445b1abb7cb..ea3a254a4d22 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false netstandard2.0 Org.OpenAPITools Org.OpenAPITools diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Org.OpenAPITools.csproj index 19789c359524..97a88c1cbe33 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false netcoreapp2.0 Org.OpenAPITools Org.OpenAPITools diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClientCoreAndNet47/src/Org.OpenAPITools/Org.OpenAPITools.csproj b/samples/client/petstore/csharp-netcore/OpenAPIClientCoreAndNet47/src/Org.OpenAPITools/Org.OpenAPITools.csproj index eaf3365f5f6d..6310b388e6e9 100644 --- a/samples/client/petstore/csharp-netcore/OpenAPIClientCoreAndNet47/src/Org.OpenAPITools/Org.OpenAPITools.csproj +++ b/samples/client/petstore/csharp-netcore/OpenAPIClientCoreAndNet47/src/Org.OpenAPITools/Org.OpenAPITools.csproj @@ -1,7 +1,7 @@ - false + false netstandard2.1;netcoreapp3.0 Org.OpenAPITools Org.OpenAPITools