diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Models/AbstractSchema.cs b/clients/algoliasearch-client-csharp/algoliasearch/Models/AbstractSchema.cs deleted file mode 100644 index 586c2944e1..0000000000 --- a/clients/algoliasearch-client-csharp/algoliasearch/Models/AbstractSchema.cs +++ /dev/null @@ -1,70 +0,0 @@ -// -// Code generated by OpenAPI Generator (https://openapi-generator.tech), manual changes will be lost - read more on https://github.com/algolia/api-clients-automation. DO NOT EDIT. -// - -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Algolia.Search.Models -{ - /// - /// Abstract base class for oneOf, anyOf schemas in the OpenAPI specification - /// - public abstract partial class AbstractSchema - { - /// - /// Custom JSON serializer - /// - static public readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings - { - // OpenAPI generated types generally hide default constructors. - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - MissingMemberHandling = MissingMemberHandling.Error, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - OverrideSpecifiedNames = false - } - } - }; - - /// - /// Custom JSON serializer for objects with additional properties - /// - static public readonly JsonSerializerSettings AdditionalPropertiesSerializerSettings = new JsonSerializerSettings - { - // OpenAPI generated types generally hide default constructors. - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - MissingMemberHandling = MissingMemberHandling.Ignore, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - OverrideSpecifiedNames = false - } - } - }; - - /// - /// Gets or Sets the actual instance - /// - public abstract Object ActualInstance { get; set; } - - /// - /// Gets or Sets IsNullable to indicate whether the instance is nullable - /// - public bool IsNullable { get; protected set; } - - /// - /// Gets or Sets the schema type, which can be either `oneOf` or `anyOf` - /// - public string SchemaType { get; protected set; } - - /// - /// Converts the instance into JSON string. - /// - public abstract string ToJson(); - } -} diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Models/Common/AbstractSchema.cs b/clients/algoliasearch-client-csharp/algoliasearch/Models/Common/AbstractSchema.cs index c333ee9101..f6f4d33d3c 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Models/Common/AbstractSchema.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Models/Common/AbstractSchema.cs @@ -1,65 +1,30 @@ using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -namespace Algolia.Search.Models.Common; - -/// -/// Abstract base class for oneOf, anyOf schemas in the API specification -/// -public abstract class AbstractSchema +namespace Algolia.Search.Models.Common { /// - /// Custom JSON serializer + /// Abstract base class for oneOf, anyOf schemas in the OpenAPI specification /// - public static readonly JsonSerializerSettings SerializerSettings = new() + public abstract partial class AbstractSchema { - // OpenAPI generated types generally hide default constructors. - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - MissingMemberHandling = MissingMemberHandling.Error, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - OverrideSpecifiedNames = false - } - } - }; - - /// - /// Custom JSON serializer for objects with additional properties - /// - public static readonly JsonSerializerSettings AdditionalPropertiesSerializerSettings = new JsonSerializerSettings - { - // OpenAPI generated types generally hide default constructors. - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - MissingMemberHandling = MissingMemberHandling.Ignore, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - OverrideSpecifiedNames = false - } - } - }; - - /// - /// Gets or Sets the actual instance - /// - public abstract object ActualInstance { get; set; } + /// + /// Gets or Sets the actual instance + /// + public abstract Object ActualInstance { get; set; } - /// - /// Gets or Sets IsNullable to indicate whether the instance is nullable - /// - public bool IsNullable { get; protected set; } + /// + /// Gets or Sets IsNullable to indicate whether the instance is nullable + /// + public bool IsNullable { get; protected set; } - /// - /// Gets or Sets the schema type, which can be either `oneOf` or `anyOf` - /// - public string SchemaType { get; protected set; } + /// + /// Gets or Sets the schema type, which can be either `oneOf` or `anyOf` + /// + public string SchemaType { get; protected set; } - /// - /// Converts the instance into JSON string. - /// - public abstract string ToJson(); + /// + /// Converts the instance into JSON string. + /// + public abstract string ToJson(); + } } diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Serializer/DefaultSerializer.cs b/clients/algoliasearch-client-csharp/algoliasearch/Serializer/DefaultSerializer.cs index 3fd471a107..b48ab8ca0b 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Serializer/DefaultSerializer.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Serializer/DefaultSerializer.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Text; using System.Threading.Tasks; using Algolia.Search.Exceptions; using Algolia.Search.Models.Common; @@ -11,29 +10,28 @@ namespace Algolia.Search.Serializer; internal class DefaultJsonSerializer : ISerializer { - private readonly JsonSerializerSettings _serializerSettings; private readonly ILogger _logger; - public DefaultJsonSerializer(JsonSerializerSettings serializerSettings, ILoggerFactory logger) + public DefaultJsonSerializer(ILoggerFactory logger) { - _serializerSettings = serializerSettings; _logger = logger.CreateLogger(); } /// /// Serialize the object into a JSON string. /// - /// Object to be serialized. + /// Object to be serialized. /// A JSON string. - public string Serialize(object obj) + public string Serialize(object data) { - if (obj is AbstractSchema schema) + if (data is AbstractSchema schema) { // the object to be serialized is an oneOf/anyOf schema - return schema.ToJson(); + var serialize = schema.ToJson(); + return serialize; } - return JsonConvert.SerializeObject(obj, _serializerSettings); + return JsonConvert.SerializeObject(data, JsonConfig.AlgoliaJsonSerializerSettings); } public async Task Deserialize(Stream response) @@ -50,37 +48,12 @@ public async Task Deserialize(Stream response) /// Object representation of the JSON string. private async Task Deserialize(Stream response, Type type) { - if (type == typeof(byte[])) // return a byte array - { - using var reader = new StreamReader(response); - return Encoding.UTF8.GetBytes(await reader.ReadToEndAsync().ConfigureAwait(false)); - } - - if (type == typeof(Stream)) - { - return response; - } - - if (type.Name.StartsWith("System.Nullable`1[[System.DateTime")) // return a datetime object - { - using var reader = new StreamReader(response); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - return DateTime.Parse(text, null, System.Globalization.DateTimeStyles.RoundtripKind); - } - - if (type == typeof(string) || type.Name.StartsWith("System.Nullable")) // return primitive type - { - using var reader = new StreamReader(response); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - return Convert.ChangeType(text, type); - } - - // Json Model try { using var reader = new StreamReader(response); - var text = await reader.ReadToEndAsync().ConfigureAwait(false); - return JsonConvert.DeserializeObject(text, type, _serializerSettings); + var readToEndAsync = await reader.ReadToEndAsync().ConfigureAwait(false); + return JsonConvert.DeserializeObject(readToEndAsync, type, + JsonConfig.AlgoliaJsonSerializerSettings); } catch (Exception ex) { diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Serializer/JsonConfig.cs b/clients/algoliasearch-client-csharp/algoliasearch/Serializer/JsonConfig.cs index 9f94902343..b07e6ec686 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Serializer/JsonConfig.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Serializer/JsonConfig.cs @@ -10,15 +10,29 @@ internal static class JsonConfig { public const string JsonContentType = "application/json"; - public static JsonSerializerSettings AlgoliaJsonSerializerSettings => new() + private static readonly DefaultContractResolver Resolver = new() { NamingStrategy = new CamelCaseNamingStrategy() }; + + public static readonly JsonSerializerSettings AlgoliaJsonSerializerSettings = new() { Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new DefaultContractResolver { NamingStrategy = new CamelCaseNamingStrategy() }, + ContractResolver = Resolver, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, DateParseHandling = DateParseHandling.DateTime, ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, MissingMemberHandling = MissingMemberHandling.Ignore, }; + + // When DeserializeOneOfSettings is used, we set MissingMemberHandling to Error to throw an exception if a property is missing + public static readonly JsonSerializerSettings DeserializeOneOfSettings = new() + { + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = Resolver, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + DateParseHandling = DateParseHandling.DateTime, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + MissingMemberHandling = MissingMemberHandling.Error, + }; } } diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Transport/CallType.cs b/clients/algoliasearch-client-csharp/algoliasearch/Transport/CallType.cs new file mode 100644 index 0000000000..3d08f2bc0b --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Transport/CallType.cs @@ -0,0 +1,21 @@ +using System; + +namespace Algolia.Search.Transport; + +/// +/// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/enumeration-types +/// Binary enums beware when adding new values +/// +[Flags] +public enum CallType +{ + /// + /// Read Call + /// + Read = 1, + + /// + /// Write Call + /// + Write = 2 +} diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpScheme.cs b/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpScheme.cs new file mode 100644 index 0000000000..b4caad26ee --- /dev/null +++ b/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpScheme.cs @@ -0,0 +1,16 @@ +namespace Algolia.Search.Transport; + +/// +/// Http Scheme +/// +public enum HttpScheme +{ + /// + /// Http + /// + Http, + /// + /// Https + /// + Https +} diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs b/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs index 106c793e5b..1baea3ffcd 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Transport/HttpTransport.cs @@ -43,7 +43,7 @@ public HttpTransport(AlgoliaConfig config, IHttpRequester httpClient, ILoggerFac _algoliaConfig = config ?? throw new ArgumentNullException(nameof(config)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _retryStrategy = new RetryStrategy(config); - _serializer = new DefaultJsonSerializer(JsonConfig.AlgoliaJsonSerializerSettings, loggerFactory); + _serializer = new DefaultJsonSerializer(loggerFactory); _logger = loggerFactory.CreateLogger(); } @@ -116,7 +116,7 @@ private async Task ExecuteRequestAsync(HttpMethod metho foreach (var host in _retryStrategy.GetTryableHost(callType)) { request.Body = CreateRequestContent(requestOptions?.Data, request.CanCompress, _logger); - request.Uri = BuildUri(host.Url, uri, requestOptions?.CustomPathParameters, requestOptions?.PathParameters, + request.Uri = BuildUri(host, uri, requestOptions?.CustomPathParameters, requestOptions?.PathParameters, requestOptions?.QueryParameters); var requestTimeout = TimeSpan.FromTicks((requestOptions?.Timeout ?? GetTimeOut(callType)).Ticks * (host.RetryCount + 1)); @@ -228,13 +228,14 @@ private IDictionary GenerateHeaders(IDictionary /// /// Build uri depending on the method /// - /// + /// /// /// /// /// /// - private static Uri BuildUri(string url, string baseUri, IDictionary customPathParameters = null, + private static Uri BuildUri(StatefulHost host, string baseUri, + IDictionary customPathParameters = null, IDictionary pathParameters = null, IDictionary optionalQueryParameters = null) { @@ -255,13 +256,19 @@ private static Uri BuildUri(string url, string baseUri, IDictionary diff --git a/clients/algoliasearch-client-csharp/algoliasearch/Transport/StatefulHost.cs b/clients/algoliasearch-client-csharp/algoliasearch/Transport/StatefulHost.cs index 02547f5096..9541a363ee 100644 --- a/clients/algoliasearch-client-csharp/algoliasearch/Transport/StatefulHost.cs +++ b/clients/algoliasearch-client-csharp/algoliasearch/Transport/StatefulHost.cs @@ -8,10 +8,20 @@ namespace Algolia.Search.Transport; public class StatefulHost { /// - /// Url endpoint without the scheme + /// Url endpoint without the scheme and the port /// public string Url { get; set; } + /// + /// Scheme of the URL + /// + public HttpScheme Scheme { get; set; } = HttpScheme.Https; + + /// + /// Port of the URL (Optional) + /// + public int? Port { get; set; } + /// /// Is the host up or not /// @@ -28,25 +38,7 @@ public class StatefulHost public DateTime LastUse { get; set; } = DateTime.UtcNow; /// - /// Calltype accepted by the host + /// CallType accepted by the host /// public CallType Accept { get; set; } } - -/// -/// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/enumeration-types -/// Binary enums beware when adding new values -/// -[Flags] -public enum CallType -{ - /// - /// Read Call - /// - Read = 1, - - /// - /// Write Call - /// - Write = 2 -} diff --git a/playground/csharp/Algolia-Old-Client/Algolia-Old-Client.csproj b/playground/csharp/Algolia-Old-Client/Algolia-Old-Client.csproj deleted file mode 100644 index 5d82839e89..0000000000 --- a/playground/csharp/Algolia-Old-Client/Algolia-Old-Client.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net8.0 - Algolia_Old_Client - enable - enable - - - - - - - diff --git a/playground/csharp/Algolia-Old-Client/Program.cs b/playground/csharp/Algolia-Old-Client/Program.cs deleted file mode 100644 index e58960d7bb..0000000000 --- a/playground/csharp/Algolia-Old-Client/Program.cs +++ /dev/null @@ -1,13 +0,0 @@ -// See https://aka.ms/new-console-template for more information - -using Algolia.Search.Clients; - -Console.WriteLine("Hello, World!"); - -var searchClient = new SearchClient("NIOXZRNMTV", "XXX"); - -var index = searchClient.InitIndex("test-csharp-legacy-client"); - -var saved = await index.SaveObjectAsync(new { ObjectID = "test", value = "test" }); - -Console.WriteLine(saved); diff --git a/playground/csharp/Algolia.sln b/playground/csharp/Algolia.sln index 10fc823b1a..94bd083bd8 100644 --- a/playground/csharp/Algolia.sln +++ b/playground/csharp/Algolia.sln @@ -4,7 +4,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playground", "Playground\Pl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Algolia.Search", "..\..\clients\algoliasearch-client-csharp\algoliasearch\Algolia.Search.csproj", "{2485A285-E565-4407-95E1-0F216142AAA8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Algolia.Search.Tests", "..\..\tests\output\csharp\src\Algolia.Search.Tests.csproj", "{5F6F1865-31E1-4984-B575-430A42217820}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Performances", "Performances\Performances.csproj", "{229F8B19-7731-47C3-9DB4-323577B95E97}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,9 +20,9 @@ Global {2485A285-E565-4407-95E1-0F216142AAA8}.Debug|Any CPU.Build.0 = Debug|Any CPU {2485A285-E565-4407-95E1-0F216142AAA8}.Release|Any CPU.ActiveCfg = Release|Any CPU {2485A285-E565-4407-95E1-0F216142AAA8}.Release|Any CPU.Build.0 = Release|Any CPU - {5F6F1865-31E1-4984-B575-430A42217820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5F6F1865-31E1-4984-B575-430A42217820}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5F6F1865-31E1-4984-B575-430A42217820}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5F6F1865-31E1-4984-B575-430A42217820}.Release|Any CPU.Build.0 = Release|Any CPU + {229F8B19-7731-47C3-9DB4-323577B95E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {229F8B19-7731-47C3-9DB4-323577B95E97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {229F8B19-7731-47C3-9DB4-323577B95E97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {229F8B19-7731-47C3-9DB4-323577B95E97}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/playground/csharp/Performances/Data/searchResult.json b/playground/csharp/Performances/Data/searchResult.json new file mode 100644 index 0000000000..46bd8e883c --- /dev/null +++ b/playground/csharp/Performances/Data/searchResult.json @@ -0,0 +1,311 @@ +{ + "hits": [ + { + "value": "batch3", + "otherValue": "otherValue3", + "objectID": "test5", + "_snippetResult": { + "value": { + "value": "batch3", + "matchLevel": "none" + }, + "otherValue": { + "value": "otherValue3", + "matchLevel": "none" + } + }, + "_highlightResult": { + "value": { + "value": "batch3", + "matchLevel": "none", + "matchedWords": [] + }, + "otherValue": { + "value": "otherValue3", + "matchLevel": "none", + "matchedWords": [] + } + }, + "_rankingInfo": { + "nbTypos": 0, + "firstMatchedWord": 0, + "proximityDistance": 0, + "userScore": 3, + "geoDistance": 0, + "geoPrecision": 0, + "nbExactWords": 0, + "words": 0, + "filters": 0 + } + }, + { + "value": "batch2", + "otherValue": "otherValue2", + "objectID": "test4", + "_snippetResult": { + "value": { + "value": "batch2", + "matchLevel": "none" + }, + "otherValue": { + "value": "otherValue2", + "matchLevel": "none" + } + }, + "_highlightResult": { + "value": { + "value": "batch2", + "matchLevel": "none", + "matchedWords": [] + }, + "otherValue": { + "value": "otherValue2", + "matchLevel": "none", + "matchedWords": [] + } + }, + "_rankingInfo": { + "nbTypos": 0, + "firstMatchedWord": 0, + "proximityDistance": 0, + "userScore": 2, + "geoDistance": 0, + "geoPrecision": 0, + "nbExactWords": 0, + "words": 0, + "filters": 0 + } + }, + { + "value": "batch1", + "otherValue": "otherValue1", + "objectID": "test3", + "_snippetResult": { + "value": { + "value": "batch1", + "matchLevel": "none" + }, + "otherValue": { + "value": "otherValue1", + "matchLevel": "none" + } + }, + "_highlightResult": { + "value": { + "value": "batch1", + "matchLevel": "none", + "matchedWords": [] + }, + "otherValue": { + "value": "otherValue1", + "matchLevel": "none", + "matchedWords": [] + } + }, + "_rankingInfo": { + "nbTypos": 0, + "firstMatchedWord": 0, + "proximityDistance": 0, + "userScore": 1, + "geoDistance": 0, + "geoPrecision": 0, + "nbExactWords": 0, + "words": 0, + "filters": 0 + } + }, + { + "value": "test", + "otherValue": "otherValue", + "objectID": "test2", + "_snippetResult": { + "value": { + "value": "test", + "matchLevel": "none" + }, + "otherValue": { + "value": "otherValue", + "matchLevel": "none" + } + }, + "_highlightResult": { + "value": { + "value": "test", + "matchLevel": "none", + "matchedWords": [] + }, + "otherValue": { + "value": "otherValue", + "matchLevel": "none", + "matchedWords": [] + } + }, + "_rankingInfo": { + "nbTypos": 0, + "firstMatchedWord": 0, + "proximityDistance": 0, + "userScore": 0, + "geoDistance": 0, + "geoPrecision": 0, + "nbExactWords": 0, + "words": 0, + "filters": 0 + } + } + ], + "nbHits": 4, + "page": 0, + "nbPages": 1, + "hitsPerPage": 10, + "facets": { + "value": { + "batch1": 1, + "batch2": 1, + "batch3": 1, + "test": 1 + }, + "otherValue": { + "otherValue": 1, + "otherValue1": 1, + "otherValue2": 1, + "otherValue3": 1 + } + }, + "exhaustiveFacetsCount": true, + "exhaustiveNbHits": true, + "exhaustiveTypo": true, + "exhaustive": { + "facetsCount": true, + "nbHits": true, + "typo": true + }, + "query": "", + "params": "analytics=false&attributesToRetrieve=%5B%22*%22%5D&attributesToSnippet=%5B%22*%3A20%22%5D&enableABTest=false&explain=%5B%22*%22%5D&facets=%5B%22*%22%5D&getRankingInfo=true&highlightPostTag=%3C%2Fais-highlight-0%3E&highlightPreTag=%3Cais-highlight-0%3E&hitsPerPage=10&maxValuesPerFacet=100&page=0&query=&responseFields=%5B%22*%22%5D&snippetEllipsisText=%E2%80%A6&tagFilters=", + "index": "test-csharp-new-client", + "serverUsed": "c42-eu-3.algolia.net", + "indexUsed": "test-csharp-new-client", + "parsedQuery": "", + "timeoutCounts": false, + "timeoutHits": false, + "explain": { + "match": { + "alternatives": [] + }, + "params": { + "client": { + "query": "", + "analytics": false, + "page": 0, + "hitsPerPage": 10, + "attributesToRetrieve": [ + "*" + ], + "attributesToSnippet": [ + "*:20" + ], + "getRankingInfo": true, + "highlightPreTag": "", + "highlightPostTag": "", + "snippetEllipsisText": "…", + "tagFilters": [], + "facets": [ + "*" + ], + "maxValuesPerFacet": 100, + "responseFields": [ + "exhaustive", + "exhaustiveFacetValues", + "facets_stats", + "extensions", + "renderingContent", + "userData", + "aroundLatLng", + "queryAfterRemoval", + "exhaustiveFacetsCount", + "length", + "params", + "processingTimingsMS", + "facets", + "offset", + "hitsPerPage", + "automaticRadius", + "processingTimeMS", + "nbPages", + "page", + "query", + "serverTimeMS", + "index", + "exhaustiveTypo", + "nbHits", + "exhaustiveNbHits", + "hits" + ], + "enableABTest": false, + "explain": true + }, + "apiKey": {}, + "abTest": {}, + "rules": {}, + "final": { + "query": "", + "analytics": false, + "page": 0, + "hitsPerPage": 10, + "attributesToRetrieve": [ + "*" + ], + "attributesToSnippet": [ + "*:20" + ], + "getRankingInfo": true, + "highlightPreTag": "", + "highlightPostTag": "", + "snippetEllipsisText": "…", + "tagFilters": [], + "facets": [ + "*" + ], + "maxValuesPerFacet": 100, + "responseFields": [ + "exhaustive", + "exhaustiveFacetValues", + "facets_stats", + "extensions", + "renderingContent", + "userData", + "aroundLatLng", + "queryAfterRemoval", + "exhaustiveFacetsCount", + "length", + "params", + "processingTimingsMS", + "facets", + "offset", + "hitsPerPage", + "automaticRadius", + "processingTimeMS", + "nbPages", + "page", + "query", + "serverTimeMS", + "index", + "exhaustiveTypo", + "nbHits", + "exhaustiveNbHits", + "hits" + ], + "enableABTest": false, + "explain": true + } + } + }, + "renderingContent": {}, + "processingTimeMS": 1, + "processingTimingsMS": { + "_request": { + "queue": 4, + "roundTrip": 16 + } + }, + "serverTimeMS": 5 +} diff --git a/playground/csharp/Performances/Model/TestObject.cs b/playground/csharp/Performances/Model/TestObject.cs new file mode 100644 index 0000000000..0d5e3eb9fe --- /dev/null +++ b/playground/csharp/Performances/Model/TestObject.cs @@ -0,0 +1,7 @@ +using Algolia.Search.Models.Search; + +public class TestObject : Hit +{ + public string? value { get; set; } + public string? otherValue { get; set; } +} diff --git a/playground/csharp/Performances/Performances.csproj b/playground/csharp/Performances/Performances.csproj new file mode 100644 index 0000000000..64e6dc5996 --- /dev/null +++ b/playground/csharp/Performances/Performances.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + Performances + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/playground/csharp/Performances/Program.cs b/playground/csharp/Performances/Program.cs new file mode 100644 index 0000000000..b5dbc712c0 --- /dev/null +++ b/playground/csharp/Performances/Program.cs @@ -0,0 +1,173 @@ +using System.Diagnostics; +using Algolia.Search.Clients; +using Algolia.Search.Models.Search; +using Algolia.Search.Transport; +using Newtonsoft.Json; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using Action = Algolia.Search.Models.Search.Action; + +var server = WireMockServer.Start(); +var client = new SearchClient(new SearchConfig("FakeAppID", "FakeApiKey") +{ + CustomHosts = + [ + new() + { + Scheme = HttpScheme.Http, + Port = server.Port, + Url = "localhost", + Accept = CallType.Read | CallType.Write, + Up = true + } + ] +}); + + +var searchResults = File.ReadAllText("Data/searchResult.json"); + +server + .Given(Request.Create().WithPath("/1/indexes/test-csharp-new-client/query").UsingPost()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(searchResults) + ); + +server + .Given(Request.Create().WithPath("/1/indexes/test-csharp-new-client/batch").UsingPost()) + .RespondWith( + Response.Create() + .WithStatusCode(200) + .WithBody(JsonConvert.SerializeObject(new BatchResponse(1, new List()))) + ); + + +Console.WriteLine("Starting performance tests..."); + +Console.WriteLine("----------------"); +Console.WriteLine("---- Search ----"); +Console.WriteLine("----------------"); + +const int iterations = 10000; +Stopwatch stopwatch = new(); + +Console.WriteLine($"Calling SearchAsync {iterations} times, with a response payload of {searchResults.Length / 1024} ko"); + +stopwatch.Start(); +for (var i = 0; i < iterations; i++) +{ + await client + .SearchSingleIndexAsync("test-csharp-new-client", new SearchParams(new SearchParamsObject())); +} + +stopwatch.Stop(); +Console.WriteLine( + $"Elapsed: {stopwatch.Elapsed.Seconds} s {stopwatch.Elapsed.Milliseconds} ms {stopwatch.Elapsed.Microseconds} μs"); + +Console.WriteLine(); + + +Console.WriteLine("-----------------------------------"); +Console.WriteLine("---- Save records, using Batch ----"); +Console.WriteLine("-----------------------------------"); + +const int iterationsBatch = 100; +const int iterationsRecords = 1000; +Console.WriteLine($"Saving {iterationsBatch*iterationsRecords} records by calling BatchAsync {iterationsBatch} times, with request payload containing {iterationsRecords} record of {JsonConvert.SerializeObject(CreateBody(0)).Length / 1024} ko."); + +stopwatch.Restart(); +for (var i = 0; i < iterationsBatch; i++) +{ + await client + .BatchAsync("test-csharp-new-client", new BatchWriteParams(CreateRequests())).ConfigureAwait(false); +} + +stopwatch.Stop(); +Console.WriteLine( + $"Elapsed: {stopwatch.Elapsed.Seconds} s {stopwatch.Elapsed.Milliseconds} ms {stopwatch.Elapsed.Microseconds} μs"); + +List CreateRequests() +{ + var requests = new List(); + + for (var i = 0; i < iterationsRecords; i++) + { + var batchRequest = new BatchRequest() + { + Action = Action.AddObject, + Body = CreateBody(i) + }; + requests.Add( + batchRequest); + } + + return requests; +} + +object CreateBody(int i1) +{ + return new + { + objectID = Guid.NewGuid(), + name = "Test" + i1, + age = i1, + city = "Paris", + country = "France", + hobbies = new[] { "sport", "music" }, + friends = new[] + { + new { name = "John", age = 42 }, + new { name = "Paul", age = 32 } + }, + company = new { name = "Algolia", country = "France", city = "Paris" }, + email = "test" + i1 + "@algolia.com", + objectIDWithNumbers = "42", + objectIDWithDates = "2020-01-01T00:00:00Z", + objectIDWithBoolean = "true", + objectIDWithArray = "[]", + objectIDWithObject = "{}", + objectIDWithNull = "null", + objectIDWithGeoPoint = "48.8584,2.2945", + objectIDWithGeoJSON = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON1 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON2 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON3 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON4 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON5 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON6 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON7 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON8 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithGeoJSON9 = "{\"type\":\"Point\",\"coordinates\":[2.2945,48.8584]}", + objectIDWithPolygon1 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon2 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon3 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon4 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon5 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon6 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon7 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon8 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon9 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithPolygon10 = "[[[-74.0059413,40.7127837],[-74.0059413,40.7127837],[-74.0059413,40.7127837]]]", + objectIDWithNullGeoPoint = "null", + objectIDWithNullGeoJSON = "null", + objectIDWithNullPolygon = "null", + objectIDWithEmptyGeoPoint = "", + objectIDWithEmptyGeoJSON = "", + objectIDWithEmptyPolygon = "", + objectIDWithEmptyArray = "", + objectIDWithEmptyObject = "", + objectIDWithEmptyString = "", + objectIDWithEmptyBoolean = "", + objectIDWithEmptyNull = "", + objectIDWithEmptyNumber = "", + objectIDWithEmptyDate = "", + objectIDWithEmptyPolygonArray = "", + objectIDWithEmptyGeoPointArray = "", + objectIDWithEmptyGeoJSONArray = "", + objectIDWithEmptyNullArray = "", + objectIDWithEmptyStringArray = "", + objectIDWithEmptyBooleanArray = "", + }; +} diff --git a/playground/csharp/Playground/Playgrounds/Monitoring.cs b/playground/csharp/Playground/Playgrounds/Monitoring.cs index 74a5f97340..04745f7827 100644 --- a/playground/csharp/Playground/Playgrounds/Monitoring.cs +++ b/playground/csharp/Playground/Playgrounds/Monitoring.cs @@ -22,7 +22,7 @@ public static async Task Run(Configuration configuration) foreach (var incident in incidentsResponse.Incidents) { Console.WriteLine( - $"{incident.Key}: {Environment.NewLine}- {string.Join($"{Environment.NewLine} -", incident.Value.Select(inner => $" { DateTimeOffset.FromUnixTimeMilliseconds(inner.T).ToString(CultureInfo.InvariantCulture)} - {inner.V.Title}"))} {Environment.NewLine}"); + $"{incident.Key}: {Environment.NewLine}- {string.Join($"{Environment.NewLine} -", incident.Value.Select(inner => $" { DateTimeOffset.FromUnixTimeMilliseconds(inner.T.Value).ToString(CultureInfo.InvariantCulture)} - {inner.V.Title}"))} {Environment.NewLine}"); } } } diff --git a/scripts/cts/runCts.ts b/scripts/cts/runCts.ts index c1b1edc611..1f2e60098e 100644 --- a/scripts/cts/runCts.ts +++ b/scripts/cts/runCts.ts @@ -6,7 +6,7 @@ async function runCtsOne(language: string): Promise { const cwd = `tests/output/${language}`; switch (language) { case 'csharp': - await run('dotnet test', { cwd, language }); + await run('dotnet test /clp:ErrorsOnly', { cwd, language }); break; case 'dart': await run('dart test', { cwd, language }); diff --git a/templates/csharp/generic_type.mustache b/templates/csharp/generic_type.mustache index d2a51d3a2e..a57ae0c8a2 100644 --- a/templates/csharp/generic_type.mustache +++ b/templates/csharp/generic_type.mustache @@ -1 +1 @@ -{{#vendorExtensions}}{{#isContainer}}{{#x-has-child-generic}}List<{{{complexType}}}>{{/x-has-child-generic}}{{^x-has-child-generic}}{{#x-propagated-generic}}List{{/x-propagated-generic}}{{^x-propagated-generic}}{{{datatypeWithEnum}}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/x-propagated-generic}}{{/x-has-child-generic}}{{/isContainer}}{{^isContainer}}{{#x-is-generic}}T{{/x-is-generic}}{{^x-is-generic}}{{{datatypeWithEnum}}}{{^isNullable}}{{#isInteger}}?{{/isInteger}}{{#isBoolean}}?{{/isBoolean}}{{/isNullable}}{{/x-is-generic}}{{/isContainer}}{{/vendorExtensions}} \ No newline at end of file +{{#vendorExtensions}}{{#isContainer}}{{#x-has-child-generic}}List<{{{complexType}}}>{{/x-has-child-generic}}{{^x-has-child-generic}}{{#x-propagated-generic}}List{{/x-propagated-generic}}{{^x-propagated-generic}}{{{datatypeWithEnum}}}{{^isContainer}}{{^required}}?{{/required}}{{/isContainer}}{{/x-propagated-generic}}{{/x-has-child-generic}}{{/isContainer}}{{^isContainer}}{{#x-is-generic}}T{{/x-is-generic}}{{^x-is-generic}}{{{datatypeWithEnum}}}{{^required}}{{^isNullable}}?{{/isNullable}}{{/required}}{{/x-is-generic}}{{/isContainer}}{{/vendorExtensions}} \ No newline at end of file diff --git a/templates/csharp/modelGeneric.mustache b/templates/csharp/modelGeneric.mustache index b8bb9df549..00c850e211 100644 --- a/templates/csharp/modelGeneric.mustache +++ b/templates/csharp/modelGeneric.mustache @@ -41,7 +41,7 @@ /// {{{.}}} {{/description}} {{^conditionalSerialization}} - [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = false)] + [DataMember(Name = "{{baseName}}")] {{#deprecated}} [Obsolete] {{/deprecated}} @@ -60,7 +60,7 @@ {{/conditionalSerialization}} {{#conditionalSerialization}} {{#isReadOnly}} - [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = false)] + [DataMember(Name = "{{baseName}}")] {{#deprecated}} [Obsolete] {{/deprecated}} @@ -78,7 +78,7 @@ {{/isReadOnly}} {{^isReadOnly}} - [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = false)] + [DataMember(Name = "{{baseName}}")] {{#deprecated}} [Obsolete] {{/deprecated}} @@ -177,7 +177,7 @@ /// {{#description}} /// {{{.}}}{{/description}} {{^conditionalSerialization}} - [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = false)] + [DataMember(Name = "{{baseName}}")] {{#isDate}} [JsonConverter(typeof(OpenAPIDateConverter))] {{/isDate}} @@ -199,7 +199,7 @@ {{/conditionalSerialization}} {{#conditionalSerialization}} {{#isReadOnly}} - [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = false)] + [DataMember(Name = "{{baseName}}")] {{#isDate}} [JsonConverter(typeof(OpenAPIDateConverter))] {{/isDate}} @@ -221,7 +221,7 @@ {{#isDate}} [JsonConverter(typeof(OpenAPIDateConverter))] {{/isDate}} - [DataMember(Name = "{{baseName}}"{{#required}}, IsRequired = true{{/required}}, EmitDefaultValue = false)] + [DataMember(Name = "{{baseName}}")] {{#deprecated}} [Obsolete] {{/deprecated}} diff --git a/templates/csharp/modelOneOf.mustache b/templates/csharp/modelOneOf.mustache index 1179311fab..a6da7bbe98 100644 --- a/templates/csharp/modelOneOf.mustache +++ b/templates/csharp/modelOneOf.mustache @@ -123,12 +123,6 @@ /// An instance of {{classname}} public static {{classname}}{{#vendorExtensions.x-has-child-generic}}{{/vendorExtensions.x-has-child-generic}} FromJson(string jsonString) { - {{classname}}{{#vendorExtensions.x-has-child-generic}}{{/vendorExtensions.x-has-child-generic}} new{{classname}} = null; - - if (string.IsNullOrEmpty(jsonString)) - { - return new{{classname}}; - } {{#useOneOfDiscriminatorLookup}} {{#discriminator}} @@ -159,7 +153,7 @@ {{#composedSchemas.oneOf}} try { - return new {{classname}}{{#x-has-child-generic}}{{/x-has-child-generic}}(JsonConvert.DeserializeObject<{{{datatypeWithEnum}}}{{#vendorExtensions.x-has-child-generic}}{{/vendorExtensions.x-has-child-generic}}>(jsonString, AdditionalPropertiesSerializerSettings)); + return new {{classname}}{{#x-has-child-generic}}{{/x-has-child-generic}}(JsonConvert.DeserializeObject<{{{datatypeWithEnum}}}{{#vendorExtensions.x-has-child-generic}}{{/vendorExtensions.x-has-child-generic}}>(jsonString, JsonConfig.DeserializeOneOfSettings)); } catch (Exception exception) { @@ -248,7 +242,7 @@ /// JSON Serializer public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - writer.WriteRawValue((string)(typeof({{classname}}{{#vendorExtensions.x-has-child-generic}}{{/vendorExtensions.x-has-child-generic}}).GetMethod("ToJson")?.Invoke(value, null))); + writer.WriteRawValue((string)value?.GetType().GetMethod("ToJson")?.Invoke(value, null)); } /// @@ -263,7 +257,7 @@ { if(reader.TokenType != JsonToken.Null) { - return objectType.GetMethod("FromJson")?.Invoke(null, new object[] { JObject.Load(reader).ToString(Formatting.None) }); + return objectType.GetMethod("FromJson")?.Invoke(null, new object[] { JToken.Load(reader).ToString(Formatting.None) }); } return null; } diff --git a/templates/csharp/tests/requests/requests.mustache b/templates/csharp/tests/requests/requests.mustache index b4fb89727f..d71867c632 100644 --- a/templates/csharp/tests/requests/requests.mustache +++ b/templates/csharp/tests/requests/requests.mustache @@ -1,6 +1,7 @@ using Algolia.Search.Http; using Algolia.Search.Clients; using Algolia.Search.Models.{{clientPrefix}}; +using Algolia.Search.Serializer; using Xunit; using Newtonsoft.Json; using Quibble.Xunit; @@ -18,7 +19,8 @@ private readonly {{client}} _client{{#hasE2E}}, _e2eClient{{/hasE2E}}; _client = new {{client}}(new {{clientPrefix}}Config("appId", "apiKey"{{#hasRegionalHost}},"{{defaultRegion}}"{{/hasRegionalHost}}), _echo); {{#hasE2E}} - DotEnv.Load(); + DotEnv.Load(options: new DotEnvOptions(ignoreExceptions: true, probeForEnv: true, probeLevelsToSearch: 8, envFilePaths: new[] { ".env" })); + var e2EAppId = Environment.GetEnvironmentVariable("ALGOLIA_APPLICATION_ID"); if (e2EAppId == null) { @@ -46,7 +48,7 @@ private readonly {{client}} _client{{#hasE2E}}, _e2eClient{{/hasE2E}}; [Fact(DisplayName = "{{{testName}}}")] public async Task {{#lambda.pascalcase}}{{method}}Test{{testIndex}}{{/lambda.pascalcase}}() { - await _client.{{#lambda.pascalcase}}{{method}}{{/lambda.pascalcase}}Async{{#isGeneric}}{{/isGeneric}}({{#parametersWithDataType}}{{> tests/generateParams}}{{^-last}},{{/-last}}{{/parametersWithDataType}}{{#hasRequestOptions}}, new RequestOptions(){ + await _client.{{#lambda.pascalcase}}{{method}}{{/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}} @@ -99,7 +101,7 @@ private readonly {{client}} _client{{#hasE2E}}, _e2eClient{{/hasE2E}}; {{#response}} // e2e try { - var resp = await _e2eClient.{{#lambda.pascalcase}}{{method}}{{/lambda.pascalcase}}Async{{#isGeneric}}{{/isGeneric}}({{#parametersWithDataType}}{{> tests/generateParams}}{{^-last}},{{/-last}}{{/parametersWithDataType}}{{#hasRequestOptions}}, new RequestOptions(){ + var resp = await _e2eClient.{{#lambda.pascalcase}}{{method}}{{/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}} @@ -113,7 +115,7 @@ private readonly {{client}} _client{{#hasE2E}}, _e2eClient{{/hasE2E}}; {{/statusCode}} {{#body}} - JsonAssert.EqualOverrideDefault("{{#lambda.escapeQuotes}}{{{.}}}{{/lambda.escapeQuotes}}", JsonConvert.SerializeObject(resp), new JsonDiffConfig(true)); + JsonAssert.EqualOverrideDefault("{{#lambda.escapeQuotes}}{{{.}}}{{/lambda.escapeQuotes}}", JsonConvert.SerializeObject(resp, settings: JsonConfig.AlgoliaJsonSerializerSettings), new JsonDiffConfig(true)); {{/body}} } catch (Exception e) {