[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
This commit is contained in:
devhl-labs
2022-01-27 23:05:36 -05:00
committed by GitHub
parent aed513f65d
commit 24366be0db
40 changed files with 3011 additions and 29 deletions

View File

@@ -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.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.</dd></dl>|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|<dl><dt>**httpclient**</dt><dd>HttpClient (https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) (Experimental. May subject to breaking changes without further notice.)</dd><dt>**restsharp**</dt><dd>RestSharp (https://github.com/restsharp/RestSharp)</dd></dl>|restsharp|
|library|HTTP library template (sub-template) to use|<dl><dt>**generichost**</dt><dd>HttpClient with Generic Host dependency injection (https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host) (Experimental. Subject to breaking changes without notice.)</dd><dt>**httpclient**</dt><dd>HttpClient (https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) (Experimental. Subject to breaking changes without notice.)</dd><dt>**restsharp**</dt><dd>RestSharp (https://github.com/restsharp/RestSharp)</dd></dl>|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|

View File

@@ -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";

View File

@@ -40,6 +40,8 @@ import static org.openapitools.codegen.utils.StringUtils.underscore;
@SuppressWarnings("Duplicates")
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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
"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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
@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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
@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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
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<Boolean> 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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
supportingFiles.add(new SupportingFile("appveyor.mustache", "", "appveyor.yml"));
supportingFiles.add(new SupportingFile("AbstractOpenAPISchema.mustache", modelPackageDir, "AbstractOpenAPISchema.cs"));
}
additionalProperties.put("apiDocPath", apiDocPath);
additionalProperties.put("modelDocPath", modelDocPath);
public void addGenericHostSupportingFiles(final String clientPackageDir, final String packageFolder,
final AtomicReference<Boolean> 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"));
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 class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
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 @@ public class CSharpNetCoreClientCodegen extends AbstractCSharpCodegen {
properties.putIfAbsent(MCS_NET_VERSION_KEY, "4.6-api");
properties.put(NET_STANDARD, strategies.stream().anyMatch(p -> Boolean.TRUE.equals(p.isNetStandard)));
for (FrameworkStrategy frameworkStrategy : frameworkStrategies) {
properties.put(frameworkStrategy.name, strategies.stream().anyMatch(s -> s.name.equals(frameworkStrategy.name)));
}
}
/**

View File

@@ -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}}

View File

@@ -0,0 +1,44 @@
// <auto-generated>
{{>partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
namespace {{packageName}}.Client
{
/// <summary>
/// API Exception
/// </summary>
{{>visibility}} class ApiException : Exception
{
/// <summary>
/// The reason the api request failed
/// </summary>
public string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ReasonPhrase { get; }
/// <summary>
/// The HttpStatusCode
/// </summary>
public System.Net.HttpStatusCode StatusCode { get; }
/// <summary>
/// The raw data returned by the api
/// </summary>
public string RawContent { get; }
/// <summary>
/// Construct the ApiException from parts of the reponse
/// </summary>
/// <param name="reasonPhrase"></param>
/// <param name="statusCode"></param>
/// <param name="rawContent"></param>
public ApiException(string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} reasonPhrase, System.Net.HttpStatusCode statusCode, string rawContent) : base(reasonPhrase ?? rawContent)
{
ReasonPhrase = reasonPhrase;
StatusCode = statusCode;
RawContent = rawContent;
}
}
}

View File

@@ -0,0 +1,59 @@
// <auto-generated>
{{partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
namespace {{packageName}}.Client
{
/// <summary>
/// A token constructed from an apiKey.
/// </summary>
public class ApiKeyToken : TokenBase
{
private string _raw;
/// <summary>
/// Constructs an ApiKeyToken object.
/// </summary>
/// <param name="value"></param>
/// <param name="prefix"></param>
/// <param name="timeout"></param>
public ApiKeyToken(string value, string prefix = "Bearer ", TimeSpan? timeout = null) : base(timeout)
{
_raw = $"{ prefix }{ value }";
}
/// <summary>
/// Places the token in the cookie.
/// </summary>
/// <param name="request"></param>
/// <param name="cookieName"></param>
public virtual void UseInCookie(System.Net.Http.HttpRequestMessage request, string cookieName)
{
request.Headers.Add("Cookie", $"{ cookieName }=_raw");
}
/// <summary>
/// Places the token in the header.
/// </summary>
/// <param name="request"></param>
/// <param name="headerName"></param>
public virtual void UseInHeader(System.Net.Http.HttpRequestMessage request, string headerName)
{
request.Headers.Add(headerName, _raw);
}
/// <summary>
/// Places the token in the query.
/// </summary>
/// <param name="request"></param>
/// <param name="uriBuilder"></param>
/// <param name="parseQueryString"></param>
/// <param name="parameterName"></param>
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}};
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Net;
namespace {{packageName}}.Client
{
/// <summary>
/// Useful for tracking server health.
/// </summary>
public class ApiResponseEventArgs : EventArgs
{
/// <summary>
/// The time the request was sent.
/// </summary>
public DateTime RequestedAt { get; }
/// <summary>
/// The time the response was received.
/// </summary>
public DateTime ReceivedAt { get; }
/// <summary>
/// The HttpStatusCode received.
/// </summary>
public HttpStatusCode HttpStatus { get; }
/// <summary>
/// The path requested.
/// </summary>
public string Path { get; }
/// <summary>
/// The elapsed time from request to response.
/// </summary>
public TimeSpan ToTimeSpan => this.ReceivedAt - this.RequestedAt;
/// <summary>
/// The event args used to track server health.
/// </summary>
/// <param name="requestedAt"></param>
/// <param name="receivedAt"></param>
/// <param name="httpStatus"></param>
/// <param name="path"></param>
public ApiResponseEventArgs(DateTime requestedAt, DateTime receivedAt, HttpStatusCode httpStatus, string path)
{
RequestedAt = requestedAt;
ReceivedAt = receivedAt;
HttpStatus = httpStatus;
Path = path;
}
}
}

View File

@@ -0,0 +1,97 @@
// <auto-generated>
{{>partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
using System.Collections.Generic;
using System.Net;
using Newtonsoft.Json;
namespace {{packageName}}.Client
{
/// <summary>
/// Provides a non-generic contract for the ApiResponse wrapper.
/// </summary>
public interface IApiResponse
{
/// <summary>
/// The data type of <see cref="Data"/>
/// </summary>
Type ResponseType { get; }
/// <summary>
/// Gets or sets the status code (HTTP status code)
/// </summary>
/// <value>The status code.</value>
HttpStatusCode StatusCode { get; }
/// <summary>
/// The raw content of this response
/// </summary>
string RawContent { get; }
}
/// <summary>
/// API Response
/// </summary>
{{>visibility}} partial class ApiResponse<T> : IApiResponse
{
#region Properties
/// <summary>
/// The deserialized content
/// </summary>
{{! .net 3.1 does not support unconstrained nullable T }}
public T{{#nullableReferenceTypes}}{{^netcoreapp3.1}}?{{/netcoreapp3.1}}{{/nullableReferenceTypes}} Content { get; set; }
/// <summary>
/// Gets or sets the status code (HTTP status code)
/// </summary>
/// <value>The status code.</value>
public HttpStatusCode StatusCode { get; }
/// <summary>
/// The content of this response
/// </summary>
public Type ResponseType
{
get { return typeof(T); }
}
/// <summary>
/// The raw data
/// </summary>
public string RawContent { get; }
/// <summary>
/// The IsSuccessStatusCode from the api response
/// </summary>
public bool IsSuccessStatusCode { get; }
/// <summary>
/// The reason phrase contained in the api response
/// </summary>
public string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ReasonPhrase { get; }
/// <summary>
/// The headers contained in the api response
/// </summary>
public System.Net.Http.Headers.HttpResponseHeaders Headers { get; }
#endregion Properties
/// <summary>
/// Construct the reponse using an HttpResponseMessage
/// </summary>
/// <param name="response"></param>
/// <param name="rawContent"></param>
public ApiResponse(System.Net.Http.HttpResponseMessage response, string rawContent)
{
StatusCode = response.StatusCode;
Headers = response.Headers;
IsSuccessStatusCode = response.IsSuccessStatusCode;
ReasonPhrase = response.ReasonPhrase;
RawContent = rawContent;
}
}
}

View File

@@ -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
{
/// <summary>
/// Base class for API tests
/// </summary>
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["<token>"], timeout: TimeSpan.FromSeconds(1));
options.AddTokens(apiKeyToken);
{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}}
BearerToken bearerToken = new BearerToken(context.Configuration["<token>"], timeout: TimeSpan.FromSeconds(1));
options.AddTokens(bearerToken);
{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}}
BasicToken basicToken = new BasicToken(context.Configuration["<username>"], context.Configuration["<password>"], timeout: TimeSpan.FromSeconds(1));
options.AddTokens(basicToken);
{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}}
HttpSigningConfiguration config = new HttpSigningConfiguration("<keyId>", "<keyFilePath>", null, new List<string>(), HashAlgorithmName.SHA256, "<signingAlgorithm>", 0);
HttpSignatureToken httpSignatureToken = new HttpSignatureToken(config, timeout: TimeSpan.FromSeconds(1));
options.AddTokens(httpSignatureToken);
{{/hasHttpSignatureMethods}}{{#hasOAuthMethods}}
OAuthToken oauthToken = new OAuthToken(context.Configuration["<token>"], timeout: TimeSpan.FromSeconds(1));
options.AddTokens(oauthToken);{{/hasOAuthMethods}}
});
}
}

View File

@@ -0,0 +1,44 @@
// <auto-generated>
{{partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace {{packageName}}.Client
{
/// <summary>
/// A token constructed from a username and password.
/// </summary>
public class BasicToken : TokenBase
{
private string _username;
private string _password;
/// <summary>
/// Constructs a BasicToken object.
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="timeout"></param>
public BasicToken(string username, string password, TimeSpan? timeout = null) : base(timeout)
{
_username = username;
_password = password;
}
/// <summary>
/// Places the token in the header.
/// </summary>
/// <param name="request"></param>
/// <param name="headerName"></param>
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));
}
}
}

View File

@@ -0,0 +1,39 @@
// <auto-generated>
{{partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace {{packageName}}.Client
{
/// <summary>
/// A token constructed from a token from a bearer token.
/// </summary>
public class BearerToken : TokenBase
{
private string _raw;
/// <summary>
/// Constructs a BearerToken object.
/// </summary>
/// <param name="value"></param>
/// <param name="timeout"></param>
public BearerToken(string value, TimeSpan? timeout = null) : base(timeout)
{
_raw = value;
}
/// <summary>
/// Places the token in the header.
/// </summary>
/// <param name="request"></param>
/// <param name="headerName"></param>
public virtual void UseInHeader(System.Net.Http.HttpRequestMessage request, string headerName)
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _raw);
}
}
}

View File

@@ -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
{
/// <summary>
/// Utility functions providing some benefit to API client consumers.
/// </summary>
public static class ClientUtils
{
{{#useCompareNetObjects}}
/// <summary>
/// An instance of CompareLogic.
/// </summary>
public static CompareLogic compareLogic;
/// <summary>
/// Static constructor to initialise compareLogic.
/// </summary>
static ClientUtils()
{
compareLogic = new CompareLogic();
}
{{/useCompareNetObjects}}
/// <summary>
/// A delegate for events.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <returns></returns>
public delegate void EventHandler<T>(object sender, T e) where T : EventArgs;
/// <summary>
/// Custom JSON serializer
/// </summary>
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
}
}
};
/// <summary>
/// Sanitize filename by removing the path
/// </summary>
/// <param name="filename">Filename</param>
/// <returns>Filename</returns>
public static string SanitizeFilename(string filename)
{
Match match = Regex.Match(filename, @".*[/\\](.*)$");
return match.Success ? match.Groups[1].Value : filename;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="obj">The parameter (header, path, query, form).</param>
/// <param name="format">The DateTime serialization format.</param>
/// <returns>Formatted string.</returns>
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<object>());
return Convert.ToString(obj, System.Globalization.CultureInfo.InvariantCulture);
}
/// <summary>
/// URL encode a string
/// Credit/Ref: https://github.com/restsharp/RestSharp/blob/master/RestSharp/Extensions/StringExtensions.cs#L50
/// </summary>
/// <param name="input">string to be URL encoded</param>
/// <returns>Byte array</returns>
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();
}
/// <summary>
/// Encode string in base64 format.
/// </summary>
/// <param name="text">string to be encoded.</param>
/// <returns>Encoded string.</returns>
public static string Base64Encode(string text)
{
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(text));
}
/// <summary>
/// Convert stream to byte array
/// </summary>
/// <param name="inputStream">Input stream to be converted</param>
/// <returns>Byte array</returns>
public static byte[] ReadAsBytes(Stream inputStream)
{
using (var ms = new MemoryStream())
{
inputStream.CopyTo(ms);
return ms.ToArray();
}
}
/// <summary>
/// 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'
/// </summary>
/// <param name="contentTypes">The Content-Type array to select from.</param>
/// <returns>The Content-Type header to use.</returns>
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'
}
/// <summary>
/// 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)
/// </summary>
/// <param name="accepts">The accepts array to select from.</param>
/// <returns>The Accept header to use.</returns>
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);
}
/// <summary>
/// Provides a case-insensitive check that a provided content type is a known JSON-like content type.
/// </summary>
public static readonly Regex JsonRegex = new Regex("(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$");
/// <summary>
/// Check if the given MIME is a JSON MIME.
/// JSON MIME examples:
/// application/json
/// application/json; charset=UTF8
/// APPLICATION/JSON
/// application/vnd.company+json
/// </summary>
/// <param name="mime">MIME</param>
/// <returns>Returns True if MIME type is json.</returns>
public static bool IsJsonMime(string mime)
{
if (string.IsNullOrWhiteSpace(mime)) return false;
return JsonRegex.IsMatch(mime) || mime.Equals("application/json-patch+json");
}
/// <summary>
/// The base path of the API
/// </summary>
public const string BASE_ADDRESS = "{{{basePath}}}";
/// <summary>
/// The scheme of the API
/// </summary>
public const string SCHEME = "{{{scheme}}}";
/// <summary>
/// The context path of the API
/// </summary>
public const string CONTEXT_PATH = "{{contextPath}}";
/// <summary>
/// The host of the API
/// </summary>
public const string HOST = "{{{host}}}";
/// <summary>
/// The format to use for DateTime serialization
/// </summary>
public const string ISO8601_DATETIME_FORMAT = "o";
/// <summary>
/// Add the api to your host builder.
/// </summary>
/// <param name="builder"></param>
/// <param name="options"></param>
public static IHostBuilder Configure{{apiName}}(this IHostBuilder builder, Action<HostBuilderContext, HostConfiguration> options)
{
builder.ConfigureServices((context, services) =>
{
HostConfiguration config = new HostConfiguration(services);
options(context, config);
Add{{apiName}}(services, config);
});
return builder;
}
/// <summary>
/// Add the api to your host builder.
/// </summary>
/// <param name="services"></param>
/// <param name="options"></param>
public static void Add{{apiName}}(this IServiceCollection services, Action<HostConfiguration> 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}}
/// <summary>
/// Adds a Polly retry policy to your clients.
/// </summary>
/// <param name="client"></param>
/// <param name="retries"></param>
/// <returns></returns>
public static IHttpClientBuilder AddRetryPolicy(this IHttpClientBuilder client, int retries)
{
client.AddPolicyHandler(RetryPolicy(retries));
return client;
}
/// <summary>
/// Adds a Polly timeout policy to your clients.
/// </summary>
/// <param name="client"></param>
/// <param name="timeout"></param>
/// <returns></returns>
public static IHttpClientBuilder AddTimeoutPolicy(this IHttpClientBuilder client, TimeSpan timeout)
{
client.AddPolicyHandler(TimeoutPolicy(timeout));
return client;
}
/// <summary>
/// Adds a Polly circiut breaker to your clients.
/// </summary>
/// <param name="client"></param>
/// <param name="handledEventsAllowedBeforeBreaking"></param>
/// <param name="durationOfBreak"></param>
/// <returns></returns>
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<HttpResponseMessage> RetryPolicy(int retries)
=> HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TimeoutRejectedException>()
.RetryAsync(retries);
private static AsyncTimeoutPolicy<HttpResponseMessage> TimeoutPolicy(TimeSpan timeout)
=> Policy.TimeoutAsync<HttpResponseMessage>(timeout);
private static Polly.CircuitBreaker.AsyncCircuitBreakerPolicy<HttpResponseMessage> CircuitBreakerPolicy(
PolicyBuilder<HttpResponseMessage> builder, int handledEventsAllowedBeforeBreaking, TimeSpan durationOfBreak)
=> builder.CircuitBreakerAsync(handledEventsAllowedBeforeBreaking, durationOfBreak);{{/supportsRetry}}
}
}

View File

@@ -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
{
/// <summary>
/// Tests the dependency injection.
/// </summary>
public class DependencyInjectionTest
{
private readonly IHost _hostUsingConfigureWithoutAClient =
Host.CreateDefaultBuilder(Array.Empty<string>()).Configure{{apiName}}((context, options) =>
{
{{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(apiKeyToken);
{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}}
BearerToken bearerToken = new BearerToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(bearerToken);
{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}}
BasicToken basicToken = new BasicToken("<username>", "<password>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(basicToken);
{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}}
HttpSigningConfiguration config = new HttpSigningConfiguration("<keyId>", "<keyFilePath>", null, new List<string>(), HashAlgorithmName.SHA256, "<signingAlgorithm>", 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<string>()).Configure{{apiName}}((context, options) =>
{
{{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(apiKeyToken);
{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}}
BearerToken bearerToken = new BearerToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(bearerToken);
{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}}
BasicToken basicToken = new BasicToken("<username>", "<password>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(basicToken);
{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}}
HttpSigningConfiguration config = new HttpSigningConfiguration("<keyId>", "<keyFilePath>", null, new List<string>(), HashAlgorithmName.SHA256, "<signingAlgorithm>", 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<string>()).ConfigureServices((host, services) =>
{
services.Add{{apiName}}(options =>
{
{{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(apiKeyToken);
{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}}
BearerToken bearerToken = new BearerToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(bearerToken);
{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}}
BasicToken basicToken = new BasicToken("<username>", "<password>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(basicToken);
{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}}
HttpSigningConfiguration config = new HttpSigningConfiguration("<keyId>", "<keyFilePath>", null, new List<string>(), HashAlgorithmName.SHA256, "<signingAlgorithm>", 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<string>()).ConfigureServices((host, services) =>
{
services.Add{{apiName}}(options =>
{
{{#hasApiKeyMethods}}ApiKeyToken apiKeyToken = new ApiKeyToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(apiKeyToken);
{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}}
BearerToken bearerToken = new BearerToken($"<token>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(bearerToken);
{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}}
BasicToken basicToken = new BasicToken("<username>", "<password>", timeout: TimeSpan.FromSeconds(1));
options.AddTokens(basicToken);
{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}}
HttpSigningConfiguration config = new HttpSigningConfiguration("<keyId>", "<keyFilePath>", null, new List<string>(), HashAlgorithmName.SHA256, "<signingAlgorithm>", 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();
/// <summary>
/// Test dependency injection when using the configure method
/// </summary>
[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}}
}
/// <summary>
/// Test dependency injection when using the configure method
/// </summary>
[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}}
}
/// <summary>
/// Test dependency injection when using the add method
/// </summary>
[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}}
}
/// <summary>
/// Test dependency injection when using the add method
/// </summary>
[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}}
}
}
}

View File

@@ -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
{
/// <summary>
/// Provides hosting configuration for {{packageName}}
/// </summary>
public class HostConfiguration
{
private readonly IServiceCollection _services;
internal bool HttpClientsAdded { get; private set; }
/// <summary>
/// Instantiates the class
/// </summary>
/// <param name="services"></param>
public HostConfiguration(IServiceCollection services)
{
_services = services;{{#apiInfo}}{{#apis}}
services.AddSingleton<{{interfacePrefix}}{{classname}}, {{classname}}>();{{/apis}}{{/apiInfo}}
}
/// <summary>
/// Configures the HttpClients.
/// </summary>
/// <param name="client"></param>
/// <param name="builder"></param>
/// <returns></returns>
public HostConfiguration Add{{apiName}}HttpClients<{{#apiInfo}}{{#apis}}T{{classname}}{{^-last}}, {{/-last}}{{/apis}}>
(
Action<HttpClient>{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} client = null, Action<IHttpClientBuilder>{{#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<IHttpClientBuilder> builders = new List<IHttpClientBuilder>();
{{#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;
}
/// <summary>
/// Configures the HttpClients.
/// </summary>
/// <param name="client"></param>
/// <param name="builder"></param>
/// <returns></returns>
public HostConfiguration Add{{apiName}}HttpClients(
Action<HttpClient>{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} client = null, Action<IHttpClientBuilder>{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} builder = null)
{
Add{{apiName}}HttpClients<{{#apiInfo}}{{#apis}}{{classname}}{{^-last}}, {{/-last}}{{/apis}}{{/apiInfo}}>(client, builder);
return this;
}
/// <summary>
/// Configures the JsonSerializerSettings
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
public HostConfiguration ConfigureJsonOptions(Action<Newtonsoft.Json.JsonSerializerSettings> options)
{
options(Client.ClientUtils.JsonSerializerSettings);
return this;
}
/// <summary>
/// Adds tokens to your IServiceCollection
/// </summary>
/// <typeparam name="TTokenBase"></typeparam>
/// <param name="token"></param>
/// <returns></returns>
public HostConfiguration AddTokens<TTokenBase>(TTokenBase token) where TTokenBase : TokenBase
{
return AddTokens(new TTokenBase[]{ token });
}
/// <summary>
/// Adds tokens to your IServiceCollection
/// </summary>
/// <typeparam name="TTokenBase"></typeparam>
/// <param name="tokens"></param>
/// <returns></returns>
public HostConfiguration AddTokens<TTokenBase>(IEnumerable<TTokenBase> tokens) where TTokenBase : TokenBase
{
TokenContainer<TTokenBase> container = new TokenContainer<TTokenBase>(tokens);
_services.AddSingleton(services => container);
return this;
}
/// <summary>
/// Adds a token provider to your IServiceCollection
/// </summary>
/// <typeparam name="TTokenProvider"></typeparam>
/// <typeparam name="TTokenBase"></typeparam>
/// <returns></returns>
public HostConfiguration UseProvider<TTokenProvider, TTokenBase>()
where TTokenProvider : TokenProvider<TTokenBase>
where TTokenBase : TokenBase
{
_services.AddSingleton<TTokenProvider>();
_services.AddSingleton<TokenProvider<TTokenBase>>(services => services.GetRequiredService<TTokenProvider>());
return this;
}
}
}

View File

@@ -0,0 +1,676 @@
// <auto-generated>
{{>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
{
/// <summary>
/// Class for HttpSigning auth related parameter and methods
/// </summary>
public class HttpSigningConfiguration
{
#region
/// <summary>
/// Create an instance
/// </summary>
public HttpSigningConfiguration(string keyId, string keyFilePath, SecureString{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} keyPassPhrase, List<string> httpSigningHeader, HashAlgorithmName hashAlgorithm, string signingAlgorithm, int signatureValidityPeriod)
{
KeyId = keyId;
KeyFilePath = keyFilePath;
KeyPassPhrase = keyPassPhrase;
HttpSigningHeader = httpSigningHeader;
HashAlgorithm = hashAlgorithm;
SigningAlgorithm = signingAlgorithm;
SignatureValidityPeriod = signatureValidityPeriod;
}
#endregion
#region Properties
/// <summary>
///Gets the Api keyId
/// </summary>
public string KeyId { get; set; }
/// <summary>
/// Gets the Key file path
/// </summary>
public string KeyFilePath { get; set; }
/// <summary>
/// Gets the key pass phrase for password protected key
/// </summary>
public SecureString{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} KeyPassPhrase { get; set; }
/// <summary>
/// Gets the HTTP signing header
/// </summary>
public List<string> HttpSigningHeader { get; set; }
/// <summary>
/// Gets the hash algorithm sha256 or sha512
/// </summary>
public HashAlgorithmName HashAlgorithm { get; set; } = HashAlgorithmName.SHA256;
/// <summary>
/// Gets the signing algorithm
/// </summary>
public string SigningAlgorithm { get; set; }
/// <summary>
/// Gets the Signature validaty period in seconds
/// </summary>
public int SignatureValidityPeriod { get; set; }
#endregion
#region enum
private enum PrivateKeyType
{
None = 0,
RSA = 1,
ECDSA = 2,
}
#endregion
#region Methods
/// <summary>
/// Gets the Headers for HttpSigning
/// </summary>
/// <param name="request"></param>
/// <param name="requestBody"></param>
/// <param name="cancellationToken"></param>
internal Dictionary<string, string> 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<string, string>();
var httpSignatureHeader = new Dictionary<string, string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>();
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<string>();
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;
}
/// <summary>
/// Gets the ECDSA signature
/// </summary>
/// <param name="dataToSign"></param>
/// <returns></returns>
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>();
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<byte>();
var sBytes = new List<byte>();
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<byte>();
tempBytes.AddRange(rBytes);
rBytes.Clear();
rBytes.Add(0x00);
rBytes.AddRange(tempBytes);
}
if (sBytes[0] > 0x7F)
{
derLength++;
sbytesLength++;
var tempBytes = new List<byte>();
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;
}
/// <summary>
/// Detect the key type from the pem file.
/// </summary>
/// <param name="keyFilePath">key file path in pem format</param>
/// <returns></returns>
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
}
}

View File

@@ -0,0 +1,43 @@
// <auto-generated>
{{partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace {{packageName}}.Client
{
/// <summary>
/// A token constructed from an HttpSigningConfiguration
/// </summary>
public class HttpSignatureToken : TokenBase
{
private HttpSigningConfiguration _configuration;
/// <summary>
/// Constructs an HttpSignatureToken object.
/// </summary>
/// <param name="configuration"></param>
/// <param name="timeout"></param>
public HttpSignatureToken(HttpSigningConfiguration configuration, TimeSpan? timeout = null) : base(timeout)
{
_configuration = configuration;
}
/// <summary>
/// Places the token in the header.
/// </summary>
/// <param name="request"></param>
/// <param name="requestBody"></param>
/// <param name="cancellationToken"></param>
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);
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Net.Http;
namespace {{packageName}}.Client
{
/// <summary>
/// Any Api client
/// </summary>
public interface {{interfacePrefix}}Api
{
/// <summary>
/// The HttpClient
/// </summary>
HttpClient HttpClient { get; }
/// <summary>
/// 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.
/// </summary>
event ClientUtils.EventHandler<ApiResponseEventArgs>{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ApiResponded;
}
}

View File

@@ -0,0 +1,39 @@
// <auto-generated>
{{partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace {{packageName}}.Client
{
/// <summary>
/// A token constructed with OAuth.
/// </summary>
public class OAuthToken : TokenBase
{
private string _raw;
/// <summary>
/// Consturcts an OAuthToken object.
/// </summary>
/// <param name="value"></param>
/// <param name="timeout"></param>
public OAuthToken(string value, TimeSpan? timeout = null) : base(timeout)
{
_raw = value;
}
/// <summary>
/// Places the token in the header.
/// </summary>
/// <param name="request"></param>
/// <param name="headerName"></param>
public virtual void UseInHeader(System.Net.Http.HttpRequestMessage request, string headerName)
{
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _raw);
}
}
}

View File

@@ -0,0 +1,214 @@
# Created with Openapi Generator
<a name="cli"></a>
## 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 "<path>/openapi-generator/modules/openapi-generator-cli/target/openapi-generator-cli.jar" generate `
-g csharp-netcore `
-i <your-swagger-file>.yaml `
-o <your-output-folder> `
--library generichost `
--additional-properties $properties `
--global-property $global `
--git-host "{{gitHost}}" `
--git-repo-id "{{gitRepoId}}" `
--git-user-id "{{gitUserId}}" `
--release-note "{{releaseNote}}"
# -t templates
```
<a name="usage"></a>
## 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("<your token>");
options.AddTokens(token);
// optionally choose the method the tokens will be provided with, default is RateLimitProvider
options.UseProvider<RateLimitProvider<ApiKeyToken>, 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
);
});
}
}
```
<a name="questions"></a>
## 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<T> 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.
<a name="dependencies"></a>
## 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}}
<a name="documentation-for-api-endpoints"></a>
## 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}}
<a name="documentation-for-models"></a>
## Documentation for Models
{{#modelPackage}}{{#models}}{{#model}} - [{{{modelPackage}}}.{{{classname}}}]({{modelDocPath}}{{{classname}}}.md){{/model}}{{/models}}{{/modelPackage}}
{{^modelPackage}}No model defined in this package{{/modelPackage}}{{/modelDocs}}
<a name="documentation-for-authorization"></a>
## Documentation for Authorization
{{^authMethods}}All endpoints do not require authorization.{{/authMethods}}{{#authMethods}}{{#-last}}Authentication schemes defined for the API:{{/-last}}{{/authMethods}}{{#authMethods}}
<a name="{{name}}"></a>
### {{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.

View File

@@ -0,0 +1,106 @@
// <auto-generated>
{{>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}}
{
/// <summary>
/// Provides a token to the api clients. Tokens will be rate limited based on the provided TimeSpan.
/// </summary>
/// <typeparam name="TTokenBase"></typeparam>
public class RateLimitProvider<TTokenBase> : TokenProvider<TTokenBase> where TTokenBase : TokenBase
{
internal Channel<TTokenBase> AvailableTokens { get; }
/// <summary>
/// Instantiates a ThrottledTokenProvider. Your tokens will be rate limited based on the token's timeout.
/// </summary>
/// <param name="container"></param>
public RateLimitProvider(TokenContainer<TTokenBase> 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<TTokenBase>(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<TTokenBase> GetAsync(System.Threading.CancellationToken? cancellation = null)
=> await AvailableTokens.Reader.ReadAsync(cancellation.GetValueOrDefault()).ConfigureAwait(false);
}
} {{/netStandard}}{{#netStandard}}
{
/// <summary>
/// Provides a token to the api clients. Tokens will be rate limited based on the provided TimeSpan.
/// </summary>
/// <typeparam name="TTokenBase"></typeparam>
public class RateLimitProvider<TTokenBase> : TokenProvider<TTokenBase> where TTokenBase : TokenBase
{
internal ConcurrentDictionary<TTokenBase, TTokenBase> AvailableTokens = new ConcurrentDictionary<TTokenBase, TTokenBase>();
private SemaphoreSlim _semaphore;
/// <summary>
/// Instantiates a ThrottledTokenProvider. Your tokens will be rate limited based on the token's timeout.
/// </summary>
/// <param name="container"></param>
public RateLimitProvider(TokenContainer<TTokenBase> 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<TTokenBase> 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}}

View File

@@ -0,0 +1,71 @@
// <auto-generated>
{{partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
namespace {{packageName}}.Client
{
/// <summary>
/// The base for all tokens.
/// </summary>
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;
/// <summary>
/// Initialize a TokenBase object.
/// </summary>
/// <param name="timeout"></param>
internal TokenBase(TimeSpan? timeout = null)
{
Timeout = timeout;
if (Timeout != null)
StartTimer(Timeout.Value);
}
/// <summary>
/// Starts the token's timer
/// </summary>
/// <param name="timeout"></param>
internal void StartTimer(TimeSpan timeout)
{
Timeout = timeout;
_timer.Interval = Timeout.Value.TotalMilliseconds;
_timer.Elapsed += OnTimer;
_timer.AutoReset = true;
_timer.Start();
}
/// <summary>
/// Returns true while the token is rate limited.
/// </summary>
public bool IsRateLimited => _nextAvailable > DateTime.UtcNow;
/// <summary>
/// Triggered when the server returns status code TooManyRequests
/// Once triggered the local timeout will be extended an arbitrary length of time.
/// </summary>
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);
}
}
}

View File

@@ -0,0 +1,37 @@
// <auto-generated>
{{partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System.Linq;
using System.Collections.Generic;
namespace {{packageName}}.Client
{
/// <summary>
/// A container for a collection of tokens.
/// </summary>
/// <typeparam name="TTokenBase"></typeparam>
public sealed class TokenContainer<TTokenBase> where TTokenBase : TokenBase
{
/// <summary>
/// The collection of tokens
/// </summary>
public List<TTokenBase> Tokens { get; } = new List<TTokenBase>();
/// <summary>
/// Instantiates a TokenContainer
/// </summary>
public TokenContainer()
{
}
/// <summary>
/// Instantiates a TokenContainer
/// </summary>
/// <param name="tokens"></param>
public TokenContainer(System.Collections.Generic.IEnumerable<TTokenBase> tokens)
{
Tokens = tokens.ToList();
}
}
}

View File

@@ -0,0 +1,36 @@
// <auto-generated>
{{>partial_header}}
{{#nullableReferenceTypes}}#nullable enable{{/nullableReferenceTypes}}
using System;
using System.Linq;
using System.Collections.Generic;
using {{packageName}}.Client;
namespace {{packageName}}
{
/// <summary>
/// A class which will provide tokens.
/// </summary>
public abstract class TokenProvider<TTokenBase> where TTokenBase : TokenBase
{
/// <summary>
/// The array of tokens.
/// </summary>
protected TTokenBase[] _tokens;
internal abstract System.Threading.Tasks.ValueTask<TTokenBase> GetAsync(System.Threading.CancellationToken? cancellation = null);
/// <summary>
/// Instantiates a TokenProvider.
/// </summary>
/// <param name="tokens"></param>
public TokenProvider(IEnumerable<TTokenBase> tokens)
{
_tokens = tokens.ToArray();
if (_tokens.Length == 0)
throw new ArgumentException("You did not provide any tokens.");
}
}
}

View File

@@ -0,0 +1,391 @@
// <auto-generated>
{{>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}}
/// <summary>
/// Represents a collection of functions to interact with the API endpoints
/// </summary>
{{>visibility}} interface {{interfacePrefix}}{{classname}} : IApi
{
{{#operation}}
/// <summary>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
{{#allParams}}
/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <returns>Task&lt;ApiResponse&lt;{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}&gt;&gt;</returns>
Task<ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>> {{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>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
{{#allParams}}
/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <returns>Task of ApiResponse&lt;{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}&gt;</returns>
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>
/// {{summary}}
/// </summary>
/// <remarks>
/// {{notes}}
/// </remarks>
{{#allParams}}
/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <returns>Task of ApiResponse&lt;{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}?&gt;</returns>
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}}
}
/// <summary>
/// Represents a collection of functions to interact with the API endpoints
/// </summary>
{{>visibility}} partial class {{classname}} : {{interfacePrefix}}{{classname}}
{
/// <summary>
/// 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.
/// </summary>
public event ClientUtils.EventHandler<ApiResponseEventArgs>{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}} ApiResponded;
/// <summary>
/// The logger
/// </summary>
public ILogger<{{classname}}> Logger { get; }
/// <summary>
/// The HttpClient
/// </summary>
public HttpClient HttpClient { get; }{{#hasApiKeyMethods}}
/// <summary>
/// A token provider of type <see cref="ApiKeyProvider"/>
/// </summary>
public TokenProvider<ApiKeyToken> ApiKeyProvider { get; }{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}}
/// <summary>
/// A token provider of type <see cref="BearerToken"/>
/// </summary>
public TokenProvider<BearerToken> BearerTokenProvider { get; }{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}}
/// <summary>
/// A token provider of type <see cref="BasicTokenProvider"/>
/// </summary>
public TokenProvider<BasicToken> BasicTokenProvider { get; }{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}}
/// <summary>
/// A token provider of type <see cref="HttpSignatureTokenProvider"/>
/// </summary>
public TokenProvider<HttpSignatureToken> HttpSignatureTokenProvider { get; }{{/hasHttpSignatureMethods}}{{#hasOAuthMethods}}
/// <summary>
/// A token provider of type <see cref="OauthTokenProvider"/>
/// </summary>
public TokenProvider<OAuthToken> OauthTokenProvider { get; }{{/hasOAuthMethods}}
/// <summary>
/// Initializes a new instance of the <see cref="{{classname}}"/> class.
/// </summary>
/// <returns></returns>
public {{classname}}(ILogger<{{classname}}> logger, HttpClient httpClient{{#hasApiKeyMethods}},
TokenProvider<ApiKeyToken> apiKeyProvider{{/hasApiKeyMethods}}{{#hasHttpBearerMethods}},
TokenProvider<BearerToken> bearerTokenProvider{{/hasHttpBearerMethods}}{{#hasHttpBasicMethods}},
TokenProvider<BasicToken> basicTokenProvider{{/hasHttpBasicMethods}}{{#hasHttpSignatureMethods}},
TokenProvider<HttpSignatureToken> httpSignatureTokenProvider{{/hasHttpSignatureMethods}}{{#hasOAuthMethods}},
TokenProvider<OAuthToken> 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>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
{{#allParams}}
/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <returns><see cref="Task"/>&lt;<see cref="{{#returnType}}{{.}}{{/returnType}}{{^returnType}}object{{/returnType}}"/>&gt;</returns>
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>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
{{#allParams}}
/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <returns><see cref="Task"/>&lt;<see cref="{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}"/>&gt;</returns>
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>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
{{#allParams}}
/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <returns><see cref="Task"/>&lt;<see cref="{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}"/>&gt;</returns>
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>
/// {{summary}} {{notes}}
/// </summary>
/// <exception cref="ApiException">Thrown when fails to make API call</exception>
{{#allParams}}
/// <param name="{{paramName}}">{{description}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}}</param>
{{/allParams}}
/// <param name="cancellationToken">Cancellation Token to cancel the request.</param>
/// <returns><see cref="Task"/>&lt;<see cref="ApiResponse{T}"/>&gt; where T : <see cref="{{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}object{{/returnType}}"/></returns>
public async Task<ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}object{{/returnType}}{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>> {{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<KeyValuePair<string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}, string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>> formParams = new List<KeyValuePair<string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}, string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>>();
multipartContent.Add(new FormUrlEncodedContent(formParams));{{/-first}}{{^isFile}}{{#required}}
formParams.Add(new KeyValuePair<string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}, string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>("{{baseName}}", ClientUtils.ParameterToString({{paramName}})));{{/required}}{{^required}}
if ({{paramName}} != null)
formParams.Add(new KeyValuePair<string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}, string{{#nullableReferenceTypes}}?{{/nullableReferenceTypes}}>("{{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<TokenBase> tokens = new List<TokenBase>();{{/-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}}
}

View File

@@ -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
{
/// <summary>
/// Class for testing {{classname}}
/// </summary>
public sealed class {{classname}}Tests : ApiTestsBase
{
private readonly {{interfacePrefix}}{{classname}} _instance;
public {{classname}}Tests(): base(Array.Empty<string>())
{
_instance = _host.Services.GetRequiredService<{{interfacePrefix}}{{classname}}>();
}
{{#operations}}
{{#operation}}
/// <summary>
/// Test {{operationId}}
/// </summary>
[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}}
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -171,6 +171,7 @@
}
}
{{#validatable}}
/// <summary>
/// To validate all properties of the instance
/// </summary>
@@ -180,6 +181,7 @@
{
yield break;
}
{{/validatable}}
}
/// <summary>

View File

@@ -216,6 +216,7 @@
}
}
{{#validatable}}
/// <summary>
/// To validate all properties of the instance
/// </summary>
@@ -225,6 +226,7 @@
{
yield break;
}
{{/validatable}}
}
/// <summary>

View File

@@ -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}}

View File

@@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<PropertyGroup>{{#useGenericHost}}
<GenerateAssemblyInfo>true</GenerateAssemblyInfo> <!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->{{/useGenericHost}}{{^useGenericHost}}
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->{{/useGenericHost}}
<TargetFramework{{#multiTarget}}s{{/multiTarget}}>{{targetFramework}}</TargetFramework{{#multiTarget}}s{{/multiTarget}}>
<AssemblyName>{{packageName}}</AssemblyName>
<PackageId>{{packageName}}</PackageId>
@@ -33,6 +34,13 @@
{{#useRestSharp}}
<PackageReference Include="RestSharp" Version="106.13.0" />
{{/useRestSharp}}
{{#useGenericHost}}
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
{{#supportsRetry}}
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="5.0.1" />
{{/supportsRetry}}
{{/useGenericHost}}
{{#supportsRetry}}
<PackageReference Include="Polly" Version="7.2.2" />
{{/supportsRetry}}

View File

@@ -4,7 +4,8 @@
<AssemblyName>{{testPackageName}}</AssemblyName>
<RootNamespace>{{testPackageName}}</RootNamespace>
<TargetFramework{{#multiTarget}}s{{/multiTarget}}>{{testTargetFramework}}</TargetFramework{{#multiTarget}}s{{/multiTarget}}>
<IsPackable>false</IsPackable>
<IsPackable>false</IsPackable>{{#nullableReferenceTypes}}
<Nullable>annotations</Nullable>{{/nullableReferenceTypes}}
</PropertyGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFramework>net47</TargetFramework>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFramework>net5.0</TargetFramework>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFramework>netcoreapp2.0</TargetFramework>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!-- setting GenerateAssemblyInfo to false causes this bug https://github.com/dotnet/project-system/issues/3934 -->
<TargetFrameworks>netstandard2.1;netcoreapp3.0</TargetFrameworks>
<AssemblyName>Org.OpenAPITools</AssemblyName>
<PackageId>Org.OpenAPITools</PackageId>