diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Clients/AlgoliaConfig.cs b/clients/algoliasearch-client-csharp/algoliasearch/Clients/AlgoliaConfig.cs index 5740c3504d..7a9a0854c4 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Clients/AlgoliaConfig.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Clients/AlgoliaConfig.cs @@ -16,21 +16,25 @@ public abstract class AlgoliaConfig private static readonly string ClientVersion = typeof(AlgoliaConfig).GetTypeInfo().Assembly.GetName().Version.ToString(); + // get the dotnet runtime version + private static readonly string DotnetVersion = Environment.Version.ToString(); + /// /// Create a new Algolia's configuration for the given credentials /// - /// Your application ID + /// Your application ID /// Your API Key - protected AlgoliaConfig(string applicationId, string apiKey) + /// The client name + protected AlgoliaConfig(string appId, string apiKey, string client) { - AppId = applicationId; + AppId = appId; ApiKey = apiKey; DefaultHeaders = new Dictionary { { Defaults.AlgoliaApplicationHeader.ToLowerInvariant(), AppId }, { Defaults.AlgoliaApiKeyHeader.ToLowerInvariant(), ApiKey }, - { Defaults.UserAgentHeader.ToLowerInvariant(), $"Algolia For Csharp {ClientVersion}" }, + { Defaults.UserAgentHeader.ToLowerInvariant(), $"Algolia for Csharp ({ClientVersion}); {client} ({ClientVersion}); Dotnet ({DotnetVersion})" }, { Defaults.Connection.ToLowerInvariant(), Defaults.KeepAlive }, { Defaults.AcceptHeader.ToLowerInvariant(), JsonConfig.JsonContentType } }; @@ -69,6 +73,11 @@ protected AlgoliaConfig(string applicationId, string apiKey) /// public TimeSpan? WriteTimeout { get; set; } + /// + /// Set the connect timeout for all requests + /// + public TimeSpan? ConnectTimeout { get; set; } + /// /// Compression for outgoing http requests /// diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Http/AlgoliaHttpRequester.cs b/clients/algoliasearch-client-csharp/algoliasearch/Http/AlgoliaHttpRequester.cs index c3ceea4457..4d8d28b7b2 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Http/AlgoliaHttpRequester.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Http/AlgoliaHttpRequester.cs @@ -27,11 +27,12 @@ internal class AlgoliaHttpRequester : IHttpRequester /// Send request to the REST API /// /// Request - /// Timeout + /// Request timeout + /// Connect timeout /// Optional cancellation token /// - public async Task SendRequestAsync(Request request, TimeSpan totalTimeout, - CancellationToken ct = default) + public async Task SendRequestAsync(Request request, TimeSpan requestTimeout, TimeSpan connectTimeout, + CancellationToken ct = default) { if (request.Method == null) { @@ -57,7 +58,7 @@ public async Task SendRequestAsync(Request request, TimeSpa } httpRequestMessage.Headers.Fill(request.Headers); - httpRequestMessage.SetTimeout(totalTimeout); + httpRequestMessage.SetTimeout(requestTimeout + connectTimeout); try { diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoHttpRequester.cs b/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoHttpRequester.cs index 202c0b6b61..b98ef192b3 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoHttpRequester.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoHttpRequester.cs @@ -21,16 +21,21 @@ private static Dictionary SplitQuery(string query) return collection.AllKeys.ToDictionary(key => key, key => collection[key]); } - public async Task SendRequestAsync(Request request, TimeSpan totalTimeout, + public async Task SendRequestAsync(Request request, TimeSpan requestTimeout, + TimeSpan connectTimeout, CancellationToken ct = default) { - EchoResponse echo = new EchoResponse(); - echo.Path = request.Uri.AbsolutePath; - echo.Host = request.Uri.Host; - echo.Method = request.Method; - echo.Body = request.Body; - echo.QueryParameters = SplitQuery(request.Uri.Query); - echo.Headers = new Dictionary(request.Headers); + EchoResponse echo = new EchoResponse + { + Path = request.Uri.AbsolutePath, + Host = request.Uri.Host, + Method = request.Method, + Body = request.Body, + QueryParameters = SplitQuery(request.Uri.Query), + Headers = new Dictionary(request.Headers), + ConnectTimeout = connectTimeout, + ResponseTimeout = requestTimeout + }; LastResponse = echo; diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoResponse.cs b/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoResponse.cs index df8160ea49..505c4af990 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoResponse.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Http/EchoResponse.cs @@ -13,7 +13,7 @@ public class EchoResponse public String Body; public Dictionary QueryParameters; public Dictionary Headers; - public int ConnectTimeout; - public int ResponseTimeout; + public TimeSpan ConnectTimeout; + public TimeSpan ResponseTimeout; } } diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Http/IHttpRequester.cs b/clients/algoliasearch-client-csharp/algoliasearch/Http/IHttpRequester.cs index ac0d8bca7e..5aaa1d8307 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Http/IHttpRequester.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Http/IHttpRequester.cs @@ -14,10 +14,10 @@ public interface IHttpRequester /// Sends the HTTP request /// /// Request object - /// Timeout + /// Request timeout + /// Connect timeout /// Optional cancellation token /// - Task SendRequestAsync(Request request, TimeSpan totalTimeout, - CancellationToken ct = default); + Task SendRequestAsync(Request request, TimeSpan requestTimeout, TimeSpan connectTimeout, CancellationToken ct = default); } } diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs b/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs index 60ef846c14..aec1978ebd 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs @@ -121,7 +121,7 @@ private async Task ExecuteRequestAsync(HttpMethod metho } AlgoliaHttpResponse response = await _httpClient - .SendRequestAsync(request, requestTimeout, ct) + .SendRequestAsync(request, requestTimeout, _algoliaConfig.ConnectTimeout ?? Defaults.ConnectTimeout, ct) .ConfigureAwait(false); _errorMessage = response.Error; diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Utils/Defaults.cs b/clients/algoliasearch-client-csharp/algoliasearch/Utils/Defaults.cs index c955a58eec..247ecf6608 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Utils/Defaults.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Utils/Defaults.cs @@ -17,6 +17,11 @@ internal class Defaults /// public static TimeSpan WriteTimeout = TimeSpan.FromSeconds(30); + /// + /// Connect timeout + /// + public static TimeSpan ConnectTimeout = TimeSpan.FromSeconds(2); + public const string AcceptHeader = "Accept"; public const string AlgoliaApplicationHeader = "X-Algolia-Application-Id"; public const string AlgoliaApiKeyHeader = "X-Algolia-API-Key"; diff --git a/templates/csharp/Configuration.mustache b/templates/csharp/Configuration.mustache index 3467783105..6b4e590872 100644 --- a/templates/csharp/Configuration.mustache +++ b/templates/csharp/Configuration.mustache @@ -18,10 +18,10 @@ namespace Algolia.Search.Clients /// The configuration of the {{packageName}} client /// A client should have it's own configuration ie on configuration per client instance /// - /// Your application ID + /// Your application ID /// Your API Key /// Targeted region {{#fallbackToAliasHost}}(optional){{/fallbackToAliasHost}} - public {{packageName}}Config(string applicationId, string apiKey, string region{{#fallbackToAliasHost}} = null{{/fallbackToAliasHost}}) : base(applicationId, apiKey) + public {{packageName}}Config(string appId, string apiKey, string region{{#fallbackToAliasHost}} = null{{/fallbackToAliasHost}}) : base(appId, apiKey, "{{packageName}}") { DefaultHosts = GetDefaultHosts(region); Compression = CompressionType.NONE; @@ -32,29 +32,29 @@ namespace Algolia.Search.Clients /// The configuration of the {{packageName}} client /// A client should have it's own configuration ie on configuration per client instance /// - /// Your application ID + /// Your application ID /// Your API Key - public {{packageName}}Config(string applicationId, string apiKey) : base(applicationId, apiKey) + public {{packageName}}Config(string appId, string apiKey) : base(appId, apiKey, "{{packageName}}") { - DefaultHosts = GetDefaultHosts(applicationId); + DefaultHosts = GetDefaultHosts(appId); Compression = CompressionType.NONE; } {{/hasRegionalHost}} {{^hasRegionalHost}} - private static List GetDefaultHosts(string applicationId) + private static List GetDefaultHosts(string appId) { List hosts = new List { new StatefulHost { - Url = $"{applicationId}-dsn.algolia.net", + Url = $"{appId}-dsn.algolia.net", Up = true, LastUse = DateTime.UtcNow, Accept = CallType.Read }, new StatefulHost { - Url = $"{applicationId}.algolia.net", Up = true, LastUse = DateTime.UtcNow, Accept = CallType.Write, + Url = $"{appId}.algolia.net", Up = true, LastUse = DateTime.UtcNow, Accept = CallType.Write, } }; @@ -62,21 +62,21 @@ namespace Algolia.Search.Clients { new StatefulHost { - Url = $"{applicationId}-1.algolianet.com", + Url = $"{appId}-1.algolianet.com", Up = true, LastUse = DateTime.UtcNow, Accept = CallType.Read | CallType.Write, }, new StatefulHost { - Url = $"{applicationId}-2.algolianet.com", + Url = $"{appId}-2.algolianet.com", Up = true, LastUse = DateTime.UtcNow, Accept = CallType.Read | CallType.Write, }, new StatefulHost { - Url = $"{applicationId}-3.algolianet.com", + Url = $"{appId}-3.algolianet.com", Up = true, LastUse = DateTime.UtcNow, Accept = CallType.Read | CallType.Write, @@ -91,10 +91,18 @@ namespace Algolia.Search.Clients private static List GetDefaultHosts(string region) { var regions = new List { {{#allowedRegions}}"{{.}}"{{^-last}},{{/-last}}{{/allowedRegions}} }; - if (region != null && !regions.Contains(region)) + {{^fallbackToAliasHost}} + if (region == null || !regions.Contains(region)) { - throw new ArgumentException($"`region` must be one of the following {regions}"); + throw new ArgumentException($"`region` is required and must be one of the following: {string.Join(", ", regions)}"); } + {{/fallbackToAliasHost}} + {{#fallbackToAliasHost}} + if(region != null && !regions.Contains(region)) + { + throw new ArgumentException($"`region` must be one of the following: {string.Join(", ", regions)}"); + } + {{/fallbackToAliasHost}} var selectedRegion = {{#fallbackToAliasHost}}region == null ? "{{{hostWithFallback}}}" : {{/fallbackToAliasHost}} "{{{regionalHost}}}".Replace("{region}", region); diff --git a/templates/csharp/libraries/httpclient/api.mustache b/templates/csharp/libraries/httpclient/api.mustache index 8457d2ab99..4a06b26f1e 100644 --- a/templates/csharp/libraries/httpclient/api.mustache +++ b/templates/csharp/libraries/httpclient/api.mustache @@ -79,19 +79,19 @@ namespace Algolia.Search.Clients { if (httpRequester == null) { - throw new ArgumentNullException(nameof(httpRequester), "An httpRequester is required"); + throw new ArgumentException("An httpRequester is required"); } if (config == null) { - throw new ArgumentNullException(nameof(config), "A config is required"); + throw new ArgumentException("A config is required"); } if (string.IsNullOrWhiteSpace(config.AppId)) { - throw new ArgumentNullException(nameof(config.AppId), "Application ID is required"); + throw new ArgumentException("`AppId` is missing."); } if (string.IsNullOrWhiteSpace(config.ApiKey)) { - throw new ArgumentNullException(nameof(config.ApiKey), "An API key is required"); + throw new ArgumentException("`ApiKey` is missing."); } _config = config; @@ -120,8 +120,7 @@ namespace Algolia.Search.Clients {{#required}} {{^vendorExtensions.x-csharp-value-type}} if ({{paramName}} == null) - throw new ApiException(400, "Missing required parameter '{{paramName}}' when calling {{classname}}->{{operationId}}"); - + throw new ApiException(400, "Parameter `{{paramName}}` is required when calling `{{operationId}}`."); {{/vendorExtensions.x-csharp-value-type}} {{/required}} {{/allParams}} diff --git a/templates/csharp/tests/client/createClient.mustache b/templates/csharp/tests/client/createClient.mustache new file mode 100644 index 0000000000..f4037ac6cd --- /dev/null +++ b/templates/csharp/tests/client/createClient.mustache @@ -0,0 +1 @@ +{{^autoCreateClient}}var client = {{/autoCreateClient}}new {{client}}(new {{clientPrefix}}Config("{{parametersWithDataTypeMap.appId.value}}","{{parametersWithDataTypeMap.apiKey.value}}"{{#hasRegionalHost}}{{#parametersWithDataTypeMap.region}},"{{parametersWithDataTypeMap.region.value}}"{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}}), _echo); diff --git a/templates/csharp/tests/client/method.mustache b/templates/csharp/tests/client/method.mustache new file mode 100644 index 0000000000..067eda92d3 --- /dev/null +++ b/templates/csharp/tests/client/method.mustache @@ -0,0 +1,9 @@ +await client.{{#lambda.pascalcase}}{{#path}}.{{.}}{{/path}}{{/lambda.pascalcase}}Async{{#isGeneric}}{{/isGeneric}}({{#parametersWithDataType}}{{> tests/generateParams}}{{^-last}},{{/-last}}{{/parametersWithDataType}}{{#hasRequestOptions}}, new RequestOptions(){ +{{#requestOptions.queryParameters.parametersWithDataType}} + QueryParameters = new Dictionary(){ {"{{{key}}}", {{> tests/requests/requestOptionsParams}} }}, +{{/requestOptions.queryParameters.parametersWithDataType}} +{{#requestOptions.headers.parametersWithDataType}} + Headers = new Dictionary(){ {"{{{key}}}", "{{{value}}}" }}, +{{/requestOptions.headers.parametersWithDataType}} + }{{/hasRequestOptions}}); +EchoResponse result = _echo.LastResponse; \ No newline at end of file diff --git a/templates/csharp/tests/client/step.mustache b/templates/csharp/tests/client/step.mustache new file mode 100644 index 0000000000..bfa9d25176 --- /dev/null +++ b/templates/csharp/tests/client/step.mustache @@ -0,0 +1,6 @@ +{{#isCreateClient}} + {{> tests/client/createClient}} +{{/isCreateClient}} +{{#isMethod}} + {{> tests/client/method}} +{{/isMethod}} \ No newline at end of file diff --git a/templates/csharp/tests/client/suite.mustache b/templates/csharp/tests/client/suite.mustache new file mode 100644 index 0000000000..9eca211632 --- /dev/null +++ b/templates/csharp/tests/client/suite.mustache @@ -0,0 +1,57 @@ +using Algolia.Search.Clients; +using Algolia.Search.Models.{{clientPrefix}}; +using Algolia.Search.Http; +using System.Text.RegularExpressions; +using Xunit; + +public class {{client}}Tests +{ +private readonly EchoHttpRequester _echo; +private Exception _ex; + +public {{client}}Tests() +{ +_echo = new EchoHttpRequester(); +} + +[Fact] +public void Dispose() +{ + +} + +{{#blocksClient}} + {{#tests}} + [Fact(DisplayName = "{{{testName}}}")] + public async Task {{#lambda.pascalcase}}{{testType}}Test{{testIndex}}{{/lambda.pascalcase}}() + { + {{#autoCreateClient}} + var client = new {{client}}(new {{clientPrefix}}Config("appId", "apiKey"{{#hasRegionalHost}},"{{defaultRegion}}"{{/hasRegionalHost}}), _echo); + {{/autoCreateClient}} + {{#steps}} + {{#isError}} + _ex = await Assert.ThrowsAnyAsync(async () => { {{> tests/client/step}} }); + Assert.Equal("{{{expectedError}}}".ToLowerInvariant(), _ex.Message.ToLowerInvariant()); + + {{/isError}} + {{^isError}} + {{> tests/client/step}} + {{#match}} + {{#testUserAgent}} { + var regexp = new Regex("{{#lambda.escapeSlash}}{{{match}}}{{/lambda.escapeSlash}}"); + Assert.Matches(regexp,result.Headers["user-agent"]); + }{{/testUserAgent}} + {{#testTimeouts}} + Assert.Equal({{{match.parametersWithDataTypeMap.connectTimeout.value}}}, result.ConnectTimeout.TotalMilliseconds); + Assert.Equal({{{match.parametersWithDataTypeMap.responseTimeout.value}}}, result.ResponseTimeout.TotalMilliseconds); + {{/testTimeouts}} + {{#testHost}} + Assert.Equal("{{{match}}}", result.Host); + {{/testHost}} + {{/match}} + {{/isError}} + {{/steps}} + } + {{/tests}} +{{/blocksClient}} + } \ No newline at end of file