diff --git a/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj b/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj index cf7b323601d..9945a675d3f 100644 --- a/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj +++ b/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj @@ -1,4 +1,5 @@ - + + Elastic.Clients.Elasticsearch Elastic.Clients.Elasticsearch - Official Elasticsearch .NET Client @@ -9,6 +10,7 @@ true README.md + true true @@ -18,24 +20,38 @@ annotations true + + + true + + + + false + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive + + <_Parameter1>true + + \ No newline at end of file diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Api/SearchRequest.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Api/SearchRequest.cs index 648e535c02f..2092393e197 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Api/SearchRequest.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Api/SearchRequest.cs @@ -36,6 +36,11 @@ protected override (string ResolvedUrl, string UrlTemplate, Dictionary : SearchRequest { + static SearchRequest() + { + DynamicallyAccessed.PublicConstructors(typeof(SearchRequestOfTConverter)); + } + public SearchRequest(Indices? indices) : base(indices) { } @@ -67,7 +72,9 @@ public override bool CanConvert(Type typeToConvert) => { var args = typeToConvert.GetGenericArguments(); +#pragma warning disable IL3050 return (JsonConverter)Activator.CreateInstance(typeof(SearchRequestOfTConverter<>).MakeGenericType(args[0])); +#pragma warning restore IL3050 } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.Esql.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.Esql.cs index 4b65cf410e6..7bf53b079db 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.Esql.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Client/ElasticsearchClient.Esql.cs @@ -71,7 +71,10 @@ private static IEnumerable EsqlToObject(ElasticsearchClient client, EsqlQu { // TODO: Improve performance + // TODO: fixme +#pragma warning disable IL2026, IL3050 using var doc = JsonSerializer.Deserialize(response.Data) ?? throw new JsonException(); +#pragma warning restore IL2026, IL3050 if (!doc.RootElement.TryGetProperty("columns"u8, out var columns) || (columns.ValueKind is not JsonValueKind.Array)) throw new JsonException(""); diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMath.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMath.cs index 91293abf686..3ba67fee432 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMath.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMath.cs @@ -45,10 +45,10 @@ internal DateMath(Union anchor, DateMathTime range, DateMathOp public static implicit operator DateMath(string dateMath) => FromString(dateMath); - public static DateMath FromString(string dateMath) + public static DateMath FromString(string? dateMath) { - if (dateMath == null) - return null; + if (dateMath is null) + throw new ArgumentNullException(nameof(dateMath)); var match = DateMathRegex.Match(dateMath); if (!match.Success) @@ -61,9 +61,12 @@ public static DateMath FromString(string dateMath) var rangeString = match.Groups["ranges"].Value; do { - var nextRangeStart = rangeString.Substring(1).IndexOfAny(new[] { '+', '-', '/' }); + var nextRangeStart = rangeString[1..].IndexOfAny(['+', '-', '/']); if (nextRangeStart == -1) + { nextRangeStart = rangeString.Length - 1; + } + var unit = rangeString.Substring(1, nextRangeStart); if (rangeString.StartsWith("+", StringComparison.Ordinal)) { @@ -73,19 +76,25 @@ public static DateMath FromString(string dateMath) else if (rangeString.StartsWith("-", StringComparison.Ordinal)) { math = math.Subtract(unit); - rangeString = rangeString.Substring(nextRangeStart + 1); + rangeString = rangeString[(nextRangeStart + 1)..]; } else - rangeString = null; + { + break; + } } while (!rangeString.IsNullOrEmpty()); } - if (match.Groups["rounding"].Success) + if (!match.Groups["rounding"].Success) { - var rounding = match.Groups["rounding"].Value.Substring(1).ToEnum(StringComparison.Ordinal); - if (rounding.HasValue) - return math.RoundTo(rounding.Value); + return math; } + + if (EnumValue.TryParse(match.Groups["rounding"].Value[1..], out var rounding)) + { + return math.RoundTo(rounding); + } + return math; } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMathTime.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMathTime.cs index ba472b8fbe3..2ad53509839 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMathTime.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/DateTime/DateMath/DateMathTime.cs @@ -79,8 +79,9 @@ public DateMathTime(string timeUnit, MidpointRounding rounding = MidpointRoundin { "M" => DateMathTimeUnit.Month, "m" => DateMathTimeUnit.Minute, - _ => intervalValue.ToEnum().GetValueOrDefault(), + _ => EnumValue.TryParse(intervalValue, out var result) ? result : default }; + SetWholeFactorIntervalAndSeconds(fraction, interval, rounding); } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/EnumValueParser.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/EnumValue.cs similarity index 66% rename from src/Elastic.Clients.Elasticsearch/_Shared/Core/EnumValueParser.cs rename to src/Elastic.Clients.Elasticsearch/_Shared/Core/EnumValue.cs index 0e5e1a821e1..691bb5a913f 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/EnumValueParser.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/EnumValue.cs @@ -2,8 +2,12 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; + #if !NET5_0_OR_GREATER + using System.Linq; + #endif namespace Elastic.Clients.Elasticsearch; @@ -12,12 +16,13 @@ namespace Elastic.Clients.Elasticsearch; using System.Collections.Generic; using System.Runtime.Serialization; -internal static class EnumValueParser +internal static class EnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T> where T : struct, Enum { private static readonly Dictionary ValueMap = new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary NameMap = new(); - static EnumValueParser() + static EnumValue() { #if NET5_0_OR_GREATER foreach (var value in Enum.GetValues()) @@ -27,12 +32,14 @@ static EnumValueParser() { var name = value.ToString(); ValueMap[name] = value; + NameMap[value] = name; var field = typeof(T).GetField(name); var attribute = (EnumMemberAttribute?)Attribute.GetCustomAttribute(field!, typeof(EnumMemberAttribute)); if (attribute?.Value is not null) { ValueMap[attribute.Value] = value; + NameMap[value] = attribute.Value; } } } @@ -51,4 +58,19 @@ public static T Parse(string input) throw new ArgumentException($"Unknown member '{input}' for enum '{typeof(T).Name}'."); } + + public static bool TryGetString(T value, [NotNullWhen(true)] out string? result) + { + return NameMap.TryGetValue(value, out result); + } + + public static string GetString(T value) + { + if (NameMap.TryGetValue(value, out var result)) + { + return result; + } + + throw new ArgumentException($"Unknown member '{value}' for enum '{typeof(T).Name}'."); + } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Extensions/Extensions.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Extensions/Extensions.cs index c990a339bb4..35c23526c15 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Extensions/Extensions.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Extensions/Extensions.cs @@ -3,14 +3,10 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection; using System.Runtime.ExceptionServices; -using System.Runtime.Serialization; -using System.Text; using System.Threading; using System.Threading.Tasks; @@ -18,100 +14,6 @@ namespace Elastic.Clients.Elasticsearch; internal static class Extensions { - private static readonly ConcurrentDictionary EnumCache = new(); - - //internal static bool NotWritable(this Query q) => q == null || !q.IsWritable; - - //internal static bool NotWritable(this IEnumerable qs) => qs == null || qs.All(q => q.NotWritable()); - - internal static string ToEnumValue(this T enumValue) where T : struct - { - var enumType = typeof(T); - var name = Enum.GetName(enumType, enumValue); - var enumMemberAttribute = enumType.GetField(name).GetCustomAttribute(); - - //if (enumMemberAttribute != null) - //return enumMemberAttribute.Value; - - //var alternativeEnumMemberAttribute = enumType.GetField(name).GetCustomAttribute(); - - return enumMemberAttribute != null - ? enumMemberAttribute.Value - : enumValue.ToString(); - } - - internal static T? ToEnum(this string str, StringComparison comparison = StringComparison.OrdinalIgnoreCase) where T : struct - { - if (str == null) - return null; - - var enumType = typeof(T); - var key = $"{enumType.Name}.{str}"; - if (EnumCache.TryGetValue(key, out var value)) - return (T)value; - - foreach (var name in Enum.GetNames(enumType)) - { - if (name.Equals(str, comparison)) - { - var v = (T)Enum.Parse(enumType, name, true); - EnumCache.TryAdd(key, v); - return v; - } - - var enumFieldInfo = enumType.GetField(name); - var enumMemberAttribute = enumFieldInfo.GetCustomAttribute(); - if (enumMemberAttribute?.Value.Equals(str, comparison) ?? false) - { - var v = (T)Enum.Parse(enumType, name); - EnumCache.TryAdd(key, v); - return v; - } - - //var alternativeEnumMemberAttribute = enumFieldInfo.GetCustomAttribute(); - //if (alternativeEnumMemberAttribute?.Value.Equals(str, comparison) ?? false) - //{ - // var v = (T)Enum.Parse(enumType, name); - // EnumCache.TryAdd(key, v); - // return v; - //} - } - - return null; - } - - internal static TReturn InvokeOrDefault(this Func func, T @default) - where T : class, TReturn where TReturn : class => - func?.Invoke(@default) ?? @default; - - internal static TReturn InvokeOrDefault(this Func func, T1 @default, - T2 param2) - where T1 : class, TReturn where TReturn : class => - func?.Invoke(@default, param2) ?? @default; - - internal static IEnumerable DistinctBy(this IEnumerable items, Func property) => - items.GroupBy(property).Select(x => x.First()); - - internal static string Utf8String(this byte[] bytes) => - bytes == null ? null : Encoding.UTF8.GetString(bytes, 0, bytes.Length); - - internal static byte[] Utf8Bytes(this string s) => s.IsNullOrEmpty() ? null : Encoding.UTF8.GetBytes(s); - - internal static bool IsNullOrEmpty(this IndexName value) => value == null || value.GetHashCode() == 0; - - internal static bool IsNullable(this Type type) => - type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - - internal static void ThrowIfNullOrEmpty(this string @object, string parameterName, string when = null) - { - @object.ThrowIfNull(parameterName, when); - if (string.IsNullOrWhiteSpace(@object)) - { - throw new ArgumentException( - "Argument can't be null or empty" + (when.IsNullOrEmpty() ? "" : " when " + when), parameterName); - } - } - // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Global internal static void ThrowIfEmpty(this IEnumerable @object, string parameterName) { @@ -164,9 +66,6 @@ internal static IEnumerable AddIfNotNull(this IEnumerable list, T other return l; } - internal static bool HasAny(this IEnumerable list, Func predicate) => - list != null && list.Any(predicate); - internal static bool HasAny(this IEnumerable list) => list != null && list.Any(); internal static bool IsNullOrEmpty(this IEnumerable? list) @@ -195,7 +94,7 @@ internal static bool IsNullOrEmptyCommaSeparatedList(this string? value, [NotNul if (string.IsNullOrWhiteSpace(value)) return true; - split = value.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + split = value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Where(t => !t.IsNullOrEmpty()) .Select(t => t.Trim()) .ToArray(); @@ -224,12 +123,6 @@ internal static void AddRangeIfNotNull(this List list, IEnumerable item list.AddRange(item.Where(x => x != null)); } - internal static Dictionary NullIfNoKeys(this Dictionary dictionary) - { - var i = dictionary?.Count; - return i.GetValueOrDefault(0) > 0 ? dictionary : null; - } - internal static async Task ForEachAsync( this IEnumerable lazyList, Func> taskSelector, @@ -297,14 +190,4 @@ long page additionalRateLimiter?.Release(); } } - - internal static bool NullOrEquals(this T o, T other) - { - if (o == null && other == null) - return true; - if (o == null || other == null) - return false; - - return o.Equals(other); - } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Extensions/TypeExtensions.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Extensions/TypeExtensions.cs deleted file mode 100644 index 331b4ed39c0..00000000000 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,190 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; - -namespace Elastic.Clients.Elasticsearch; - -internal static class TypeExtensions -{ - internal static readonly MethodInfo GetActivatorMethodInfo = - typeof(TypeExtensions).GetMethod(nameof(GetActivator), BindingFlags.Static | BindingFlags.NonPublic); - - private static readonly ConcurrentDictionary> CachedActivators = - new(); - - private static readonly ConcurrentDictionary CachedGenericClosedTypes = - new(); - - private static readonly ConcurrentDictionary> CachedTypePropertyInfos = - new(); - - internal static object CreateGenericInstance(this Type t, Type closeOver, params object[] args) => - t.CreateGenericInstance(new[] { closeOver }, args); - - internal static object CreateGenericInstance(this Type t, Type[] closeOver, params object[] args) - { - var key = closeOver.Aggregate(new StringBuilder(t.FullName), (sb, gt) => - { - sb.Append("--"); - return sb.Append(gt.FullName); - }, sb => sb.ToString()); - if (!CachedGenericClosedTypes.TryGetValue(key, out var closedType)) - { - closedType = t.MakeGenericType(closeOver); - CachedGenericClosedTypes.TryAdd(key, closedType); - } - return closedType.CreateInstance(args); - } - - internal static T CreateInstance(this Type t, params object[] args) => (T)t.CreateInstance(args); - - internal static object CreateInstance(this Type t, params object[] args) - { - var key = t.FullName; - var argKey = args.Length; - if (args.Length > 0) - key = argKey + "--" + key; - if (CachedActivators.TryGetValue(key, out var activator)) - return activator(args); - - var generic = GetActivatorMethodInfo.MakeGenericMethod(t); - var constructors = from c in t.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - let p = c.GetParameters() - where p.Length == args.Length - select c; - - var ctor = constructors.FirstOrDefault(); - if (ctor == null) - throw new Exception($"Cannot create an instance of {t.FullName} because it has no constructor taking {args.Length} arguments"); - - activator = (ObjectActivator)generic.Invoke(null, new object[] { ctor }); - CachedActivators.TryAdd(key, activator); - return activator(args); - } - - //do not remove this is referenced through GetActivatorMethod - internal static ObjectActivator GetActivator(ConstructorInfo ctor) - { - var paramsInfo = ctor.GetParameters(); - - //create a single param of type object[] - var param = Expression.Parameter(typeof(object[]), "args"); - - var argsExp = new Expression[paramsInfo.Length]; - - //pick each arg from the params array - //and create a typed expression of them - for (var i = 0; i < paramsInfo.Length; i++) - { - var index = Expression.Constant(i); - var paramType = paramsInfo[i].ParameterType; - var paramAccessorExp = Expression.ArrayIndex(param, index); - var paramCastExp = Expression.Convert(paramAccessorExp, paramType); - argsExp[i] = paramCastExp; - } - - //make a NewExpression that calls the - //ctor with the args we just created - var newExp = Expression.New(ctor, argsExp); - - //create a lambda with the New - //Expression as body and our param object[] as arg - var lambda = Expression.Lambda(typeof(ObjectActivator), newExp, param); - - //compile it - var compiled = (ObjectActivator)lambda.Compile(); - return compiled; - } - - internal static List GetAllProperties(this Type type) - { - if (CachedTypePropertyInfos.TryGetValue(type, out var propertyInfos)) - return propertyInfos; - - var properties = new Dictionary(); - GetAllPropertiesCore(type, properties); - propertyInfos = properties.Values.ToList(); - CachedTypePropertyInfos.TryAdd(type, propertyInfos); - return propertyInfos; - } - - /// - /// Returns inherited properties with reflectedType set to base type - /// - private static void GetAllPropertiesCore(Type type, Dictionary collectedProperties) - { - foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) - { - if (collectedProperties.TryGetValue(property.Name, out var existingProperty)) - { - if (IsHidingMember(property)) - collectedProperties[property.Name] = property; - else if (!type.IsInterface && property.IsVirtual()) - { - var propertyDeclaringType = property.GetDeclaringType(); - - if (!(existingProperty.IsVirtual() && existingProperty.GetDeclaringType().IsAssignableFrom(propertyDeclaringType))) - collectedProperties[property.Name] = property; - } - } - else - collectedProperties.Add(property.Name, property); - } - - if (type.BaseType != null) - GetAllPropertiesCore(type.BaseType, collectedProperties); - } - - /// - /// Determines if a is hiding/shadowing a - /// from a base type - /// - private static bool IsHidingMember(PropertyInfo propertyInfo) - { - var baseType = propertyInfo.DeclaringType?.BaseType; - var baseProperty = baseType?.GetProperty(propertyInfo.Name); - if (baseProperty == null) - return false; - - var derivedGetMethod = propertyInfo.GetBaseDefinition(); - return derivedGetMethod?.ReturnType != propertyInfo.PropertyType; - } - - private static Type GetDeclaringType(this PropertyInfo propertyInfo) => - propertyInfo.GetBaseDefinition()?.DeclaringType ?? propertyInfo.DeclaringType; - - private static MethodInfo GetBaseDefinition(this PropertyInfo propertyInfo) - { - var m = propertyInfo.GetMethod; - return m != null - ? m.GetBaseDefinition() - : propertyInfo.SetMethod?.GetBaseDefinition(); - } - - /// - /// Determines if a is virtual - /// - private static bool IsVirtual(this PropertyInfo propertyInfo) - { - var methodInfo = propertyInfo.GetMethod; - if (methodInfo != null && methodInfo.IsVirtual) - return true; - - methodInfo = propertyInfo.SetMethod; - return methodInfo != null && methodInfo.IsVirtual; - } - - internal delegate T ObjectActivator(params object[] args); - - private static readonly Assembly ElasticsearchClientsElasticsearchAssembly = typeof(TypeExtensions).Assembly; - - public static bool IsElasticsearchClientsElasticsearchType(this Type type) => type.Assembly == ElasticsearchClientsElasticsearchAssembly; -} diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/DynamicPropertyAccessor.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/DynamicPropertyAccessor.cs new file mode 100644 index 00000000000..74e8aaf3bea --- /dev/null +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/DynamicPropertyAccessor.cs @@ -0,0 +1,111 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +#if NETCOREAPP3_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif + +namespace Elastic.Clients.Elasticsearch; + +internal static class DynamicPropertyAccessor +{ + public static Func CreateGetterDelegate( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, + PropertyInfo property, + Func, object, TResult?>? retrieverFunc = null) + { + if (type is null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (property is null) + { + throw new ArgumentNullException(nameof(property)); + } + + var getterMethod = property.GetMethod; + if (getterMethod is null) + { + throw new ArgumentException($"Property '{property.Name}' of '{type.Name}' does declare a getter method."); + } + + retrieverFunc ??= static (genericGetter, instance) => (TResult?)genericGetter(instance); + +#if NETCOREAPP3_0_OR_GREATER + + if (!RuntimeFeature.IsDynamicCodeSupported || !RuntimeFeature.IsDynamicCodeCompiled) + { + // Fall back to reflection based getter access. + + return instance => retrieverFunc(instance => getterMethod.Invoke(instance, []), instance); + } + +#endif + + // Build compiled getter delegate. + +#pragma warning disable IL3050 + var getterDelegateFactory = MakeDelegateMethodInfo.MakeGenericMethod(type, getterMethod.ReturnType); +#pragma warning restore IL3050 + var genericGetterDelegate = (Func)getterDelegateFactory.Invoke(null, [getterMethod])!; + + return instance => retrieverFunc(genericGetterDelegate, instance); + } + + public static Func CreateGetterDelegate( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, + string propertyName, + Func, object, TResult?>? retrieverFunc = null) + { + return TryCreateGetterDelegate(type, propertyName, out var result, retrieverFunc) + ? result + : throw new ArgumentException($"Type '{type.Name}' does not have a public property with name '{propertyName}'."); + } + + public static bool TryCreateGetterDelegate( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, + string propertyName, + [NotNullWhen(true)] out Func? result, + Func, object, TResult?>? retrieverFunc = null) + { + if (type is null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (propertyName is null) + { + throw new ArgumentNullException(nameof(propertyName)); + } + + var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (property is null) + { + result = null; + return false; + } + + result = CreateGetterDelegate(type, property, retrieverFunc); + return true; + } + + private static readonly MethodInfo MakeDelegateMethodInfo = typeof(DynamicPropertyAccessor).GetMethod(nameof(MakeDelegate), BindingFlags.Static | BindingFlags.NonPublic)!; + + private static Func MakeDelegate(MethodInfo getterMethod) + { +#if NET5_0_OR_GREATER + var func = getterMethod.CreateDelegate>(); //(Func)getterMethod.CreateDelegate(typeof(Func)); +#else + var func = (Func)getterMethod.CreateDelegate(typeof(Func)); +#endif + + return type => func((T)type); + } +} diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs index b7f5bd46b30..59ad5ebc601 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs @@ -4,90 +4,84 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Reflection; namespace Elastic.Clients.Elasticsearch; public class IdResolver { - private static readonly ConcurrentDictionary> IdDelegates = new(); + private static readonly ConcurrentDictionary?> GlobalDelegateCache = new(); - private static readonly MethodInfo MakeDelegateMethodInfo = - typeof(IdResolver).GetMethod(nameof(MakeDelegate), BindingFlags.Static | BindingFlags.NonPublic); + private readonly IElasticsearchClientSettings _settings; + private readonly ConcurrentDictionary?> _localDelegateCache = new(); - private readonly IElasticsearchClientSettings _elasticsearchClientSettings; - private readonly ConcurrentDictionary> _localIdDelegates = new(); - - public IdResolver(IElasticsearchClientSettings elasticsearchClientSettings) => - _elasticsearchClientSettings = elasticsearchClientSettings; - - private PropertyInfo GetPropertyCaseInsensitive(Type type, string fieldName) - => type.GetProperty(fieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - - internal Func CreateIdSelector() where T : class + public IdResolver(IElasticsearchClientSettings settings) { - Func idSelector = Resolve; - return idSelector; + _settings = settings; } - internal static Func MakeDelegate(MethodInfo @get) + public string? Resolve<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>(T instance) { - var f = (Func)@get.CreateDelegate(typeof(Func)); - return t => f((T)t); - } + if (_settings.DefaultDisableIdInference || (instance is null)) + { + return null; + } - public string Resolve(T @object) => - _elasticsearchClientSettings.DefaultDisableIdInference || @object == null - ? null - : Resolve(@object.GetType(), @object); + return Resolve(instance.GetType(), instance); + } - public string Resolve(Type type, object @object) + public string? Resolve([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, object instance) { - if (type == null || @object == null) - return null; - if (_elasticsearchClientSettings.DefaultDisableIdInference || - _elasticsearchClientSettings.DisableIdInference.Contains(type)) + if (type is null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (instance is null) + { + throw new ArgumentNullException(nameof(instance)); + } + + if (_settings.DefaultDisableIdInference || _settings.DisableIdInference.Contains(type)) + { return null; + } - var preferLocal = _elasticsearchClientSettings.IdProperties.TryGetValue(type, out _); + var localIdPropertyName = + _settings.IdProperties.TryGetValue(type, out var propertyName) && !string.IsNullOrEmpty(propertyName) + ? propertyName + : null; + var idPropertyName = localIdPropertyName ?? "Id"; - if (_localIdDelegates.TryGetValue(type, out var cachedLookup)) - return cachedLookup(@object); + var delegateCache = string.IsNullOrEmpty(localIdPropertyName) ? GlobalDelegateCache : _localDelegateCache; + if (delegateCache.TryGetValue(type, out var getterDelegate)) + { + return getterDelegate?.Invoke(instance); + } - if (!preferLocal && IdDelegates.TryGetValue(type, out cachedLookup)) - return cachedLookup(@object); + if (!DynamicPropertyAccessor.TryCreateGetterDelegate(type, idPropertyName, out getterDelegate, GetAndFormatAsString)) + { + if (!string.IsNullOrEmpty(localIdPropertyName)) + { + throw new ArgumentException($"Type '{type.Name}' does not have a public property with name '{localIdPropertyName}'."); + } + + // Avoid reflection calls for subsequent invocations. + delegateCache.TryAdd(type, null); - var idProperty = GetInferredId(type); - if (idProperty == null) return null; + } - var getMethod = idProperty.GetMethod; - var generic = MakeDelegateMethodInfo.MakeGenericMethod(type, getMethod.ReturnType); - var func = (Func)generic.Invoke(null, new object[] {getMethod}); - cachedLookup = o => - { - var v = func(o); - return (v is IFormattable f) ? f.ToString(null, CultureInfo.InvariantCulture) : v?.ToString(); - }; - if (preferLocal) - _localIdDelegates.TryAdd(type, cachedLookup); - else - IdDelegates.TryAdd(type, cachedLookup); - return cachedLookup(@object); + return getterDelegate(instance); } - private PropertyInfo GetInferredId(Type type) + private static string? GetAndFormatAsString(Func genericGetter, object instance) { - // if the type specifies through ElasticAttribute what the id prop is - // use that no matter what - - _elasticsearchClientSettings.IdProperties.TryGetValue(type, out var propertyName); - if (!propertyName.IsNullOrEmpty()) - return GetPropertyCaseInsensitive(type, propertyName); - - propertyName = "Id"; + var value = genericGetter(instance); - return GetPropertyCaseInsensitive(type, propertyName); + return (value is IFormattable formattable) + ? formattable.ToString(null, CultureInfo.InvariantCulture) + : value?.ToString(); } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdsConverter.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdsConverter.cs index 0fcada2b2fc..3d16fd1188b 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdsConverter.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdsConverter.cs @@ -3,28 +3,18 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; +using Elastic.Clients.Elasticsearch.Serialization; + namespace Elastic.Clients.Elasticsearch; internal sealed class IdsConverter : JsonConverter { public override Ids? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonTokenType.StartArray) - throw new JsonException($"Unexpected JSON token. Expected {JsonTokenType.StartArray} but read {reader.TokenType}"); - - var ids = new List(); - - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - var id = JsonSerializer.Deserialize(ref reader, options); - - if (id is not null) - ids.Add(id); - } + var ids = reader.ReadCollectionValue(options, null)!; return new Ids(ids); } @@ -33,17 +23,9 @@ public override void Write(Utf8JsonWriter writer, Ids value, JsonSerializerOptio { if (value is null) { - writer.WriteNullValue(); - return; - } - - writer.WriteStartArray(); - - foreach (var id in value.IdsToSerialize) - { - JsonSerializer.Serialize(writer, id, options); + throw new ArgumentNullException(nameof(value)); } - writer.WriteEndArray(); + writer.WriteCollectionValue(options, value.IdsToSerialize, null); } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Inferrer.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Inferrer.cs index 4d5c459660f..83ff425e4f0 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Inferrer.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Inferrer.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics.CodeAnalysis; + using Elastic.Transport; namespace Elastic.Clients.Elasticsearch; @@ -20,23 +22,8 @@ public Inferrer(IElasticsearchClientSettings elasticsearchClientSettings) RelationNameResolver = new RelationNameResolver(elasticsearchClientSettings); FieldResolver = new FieldResolver(elasticsearchClientSettings); RoutingResolver = new RoutingResolver(elasticsearchClientSettings, IdResolver); - - //CreateMultiHitDelegates = - // new ConcurrentDictionary>>>(); - //CreateSearchResponseDelegates = - // new ConcurrentDictionary>>(); } - //internal ConcurrentDictionary>> - // > - // CreateMultiHitDelegates { get; } - - //internal ConcurrentDictionary>> - // CreateSearchResponseDelegates { get; } - private FieldResolver FieldResolver { get; } private IdResolver IdResolver { get; } private IndexNameResolver IndexNameResolver { get; } @@ -53,15 +40,15 @@ public Inferrer(IElasticsearchClientSettings elasticsearchClientSettings) public string IndexName(IndexName index) => IndexNameResolver.Resolve(index); - public string Id(T instance) => IdResolver.Resolve(instance); + public string? Id<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>(T instance) => IdResolver.Resolve(instance); - public string Id(Type type, object instance) => IdResolver.Resolve(type, instance); + public string? Id([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, object instance) => IdResolver.Resolve(type, instance); public string RelationName() => RelationNameResolver.Resolve(); public string RelationName(RelationName type) => RelationNameResolver.Resolve(type); - public string Routing(T document) => RoutingResolver.Resolve(document); + public string? Routing<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>(T document) => RoutingResolver.Resolve(document); - public string Routing(Type type, object instance) => RoutingResolver.Resolve(type, instance); + public string? Routing([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, object instance) => RoutingResolver.Resolve(type, instance); } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/JoinFieldConverter.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/JoinFieldConverter.cs index 7aff28193fd..78367211175 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/JoinFieldConverter.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/JoinFieldConverter.cs @@ -7,44 +7,47 @@ using System.Text.Json; using Elastic.Transport; using Elastic.Clients.Elasticsearch.Serialization; -using System.Runtime; namespace Elastic.Clients.Elasticsearch; internal sealed class JoinFieldConverter : JsonConverter { - private IElasticsearchClientSettings _settings; + private static readonly JsonEncodedText PropName = JsonEncodedText.Encode("name"); + private static readonly JsonEncodedText PropParent = JsonEncodedText.Encode("parent"); public override JoinField? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { - var parent = reader.GetString(); + var parent = reader.GetString()!; return new JoinField(new JoinField.Parent(parent)); } - Id parentId = null; - string name = null; + reader.ValidateToken(JsonTokenType.StartObject); - while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) - { - if (reader.TokenType != JsonTokenType.PropertyName) - throw new JsonException($"Unexpected token. Expected {JsonTokenType.PropertyName}, but read {reader.TokenType}."); + Id? parentId = null; + string? name = null; - var propertyName = reader.GetString(); - reader.Read(); + while (reader.Read() && reader.TokenType is JsonTokenType.PropertyName) + { + if (reader.TryReadProperty(options, PropName, ref name, null)) + { + continue; + } - switch (propertyName) + if (reader.TryReadProperty(options, PropParent, ref parentId, null)) { - case "parent": - parentId = JsonSerializer.Deserialize(ref reader, options); - break; - case "name": - name = reader.GetString(); - break; - default: - throw new JsonException($"Read an unexpected property name {propertyName}."); + continue; } + + throw new JsonException($"Read an unexpected property name {reader.GetString()!}."); + } + + reader.ValidateToken(JsonTokenType.EndObject); + + if (name is null) + { + throw new JsonException("Missing required property 'name'."); } return parentId != null @@ -56,37 +59,21 @@ public override void Write(Utf8JsonWriter writer, JoinField value, JsonSerialize { if (value is null) { - writer.WriteNullValue(); - return; + throw new ArgumentNullException(nameof(value)); } switch (value.Tag) { case 0: - JsonSerializer.Serialize(writer, value.ParentOption.Name, options); + writer.WriteValue(options, value.ParentOption.Name); break; case 1: - InitializeSettings(options); writer.WriteStartObject(); - writer.WritePropertyName("name"); - JsonSerializer.Serialize(writer, value.ChildOption.Name, options); - writer.WritePropertyName("parent"); - var id = (value.ChildOption.ParentId as IUrlParameter)?.GetString(_settings); - writer.WriteStringValue(id); + writer.WriteProperty(options, PropName, value.ChildOption.Name); + writer.WriteProperty(options, PropParent, (value.ChildOption.ParentId as IUrlParameter)?.GetString(options.GetContext())); writer.WriteEndObject(); break; } } - - private void InitializeSettings(JsonSerializerOptions options) - { - if (_settings is null) - { - if (!options.TryGetClientSettings(out var settings)) - ThrowHelper.ThrowJsonExceptionForMissingSettings(); - - _settings = settings; - } - } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs index dac444251ac..b950e5c082e 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs @@ -4,122 +4,127 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Linq; +using System.Globalization; namespace Elastic.Clients.Elasticsearch; public class RoutingResolver { - private static readonly ConcurrentDictionary> PropertyGetDelegates = - new(); + private static readonly ConcurrentDictionary?> JoinFieldDelegateCache = new(); - private static readonly MethodInfo MakeDelegateMethodInfo = - typeof(RoutingResolver).GetMethod(nameof(MakeDelegate), BindingFlags.Static | BindingFlags.NonPublic); + private readonly IElasticsearchClientSettings _settings; + private readonly ConcurrentDictionary?> _routeDelegateCache = new(); - - private readonly IElasticsearchClientSettings _transportClientSettings; - private readonly IdResolver _idResolver; - - private readonly ConcurrentDictionary> - _localRouteDelegates = new(); - - public RoutingResolver(IElasticsearchClientSettings connectionSettings, IdResolver idResolver) + public RoutingResolver(IElasticsearchClientSettings settings, IdResolver idResolver) { - _transportClientSettings = connectionSettings; - _idResolver = idResolver; + _settings = settings; } - private PropertyInfo GetPropertyCaseInsensitive(Type type, string fieldName) => - type.GetProperty(fieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); - - internal static Func MakeDelegate(MethodInfo @get) + public string? Resolve<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>(T instance) { - var f = (Func)@get.CreateDelegate(typeof(Func)); - return t => f((T)t); - } + if (instance is null) + { + return null; + } - public string Resolve(T @object) => @object == null ? null : Resolve(@object.GetType(), @object); + return Resolve(instance.GetType(), instance); + } - public string Resolve(Type type, object @object) + public string? Resolve([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, object instance) { - if (TryConnectionSettingsRoute(type, @object, out var route)) + if (type is null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (instance is null) + { + throw new ArgumentNullException(nameof(instance)); + } + + if (TryGetConnectionSettingsRoute(type, instance, out var route)) + { return route; + } + + return GetJoinFieldFromObject(type, instance)?.Match(_ => _settings.Inferrer.Id(instance), child => ResolveId(_settings, child.ParentId)); - var joinField = GetJoinFieldFromObject(type, @object); - return joinField?.Match(p => _idResolver.Resolve(@object), c => ResolveId(c.ParentId, _transportClientSettings)); + static string? ResolveId(IElasticsearchClientSettings settings, Id id) + { + return (id.Document is not null) ? settings.Inferrer.Id(id.Document) : id.StringOrLongValue; + } } - private bool TryConnectionSettingsRoute(Type type, object @object, out string route) + private bool TryGetConnectionSettingsRoute( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, + object instance, + out string? route) { - route = null; - if (!_transportClientSettings.RouteProperties.TryGetValue(type, out var propertyName)) + if (!_settings.RouteProperties.TryGetValue(type, out var propertyName)) + { + route = null; return false; + } - if (_localRouteDelegates.TryGetValue(type, out var cachedLookup)) + if (_routeDelegateCache.TryGetValue(type, out var getterDelegate)) { - route = cachedLookup(@object); + route = getterDelegate?.Invoke(instance); return true; } - var property = GetPropertyCaseInsensitive(type, propertyName); - var func = CreateGetterFunc(type, property); - cachedLookup = o => - { - var v = func(o); - return v?.ToString(); - }; - _localRouteDelegates.TryAdd(type, cachedLookup); - route = cachedLookup(@object); + + getterDelegate = DynamicPropertyAccessor.CreateGetterDelegate(type, propertyName, GetAndFormatAsString); + _routeDelegateCache.TryAdd(type, getterDelegate); + + route = getterDelegate(instance); return true; } - private string ResolveId(Id id, IElasticsearchClientSettings settings) => - id.Document != null ? settings.Inferrer.Id(id.Document) : id.StringOrLongValue; - - private static JoinField GetJoinFieldFromObject(Type type, object @object) + private static JoinField? GetJoinFieldFromObject( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, + object instance) { - if (type == null || @object == null) - return null; - - if (PropertyGetDelegates.TryGetValue(type, out var cachedLookup)) - return cachedLookup(@object); + if (JoinFieldDelegateCache.TryGetValue(type, out var getterDelegate)) + { + return getterDelegate?.Invoke(instance); + } var joinProperty = GetJoinFieldProperty(type); - if (joinProperty == null) + if (joinProperty is null) { - PropertyGetDelegates.TryAdd(type, o => null); + // Avoid reflection calls for subsequent invocations. + JoinFieldDelegateCache.TryAdd(type, null); + return null; } - var func = CreateGetterFunc(type, joinProperty); - cachedLookup = o => + getterDelegate = DynamicPropertyAccessor.CreateGetterDelegate(type, joinProperty); + JoinFieldDelegateCache.TryAdd(type, getterDelegate); + + return getterDelegate(instance); + + static PropertyInfo? GetJoinFieldProperty([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type) { - var v = func(o); - return v as JoinField; - }; - PropertyGetDelegates.TryAdd(type, cachedLookup); - return cachedLookup(@object); + var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); + try + { + return properties.SingleOrDefault(p => p.PropertyType == typeof(JoinField)); + } + catch (InvalidOperationException e) + { + throw new ArgumentException($"Type '{type.Name}' has more than one '{nameof(JoinField)}' property.", e); + } + } } - private static Func CreateGetterFunc(Type type, PropertyInfo joinProperty) + private static string? GetAndFormatAsString(Func genericGetter, object instance) { - var getMethod = joinProperty.GetMethod; - var generic = MakeDelegateMethodInfo.MakeGenericMethod(type, getMethod.ReturnType); - var func = (Func)generic.Invoke(null, new object[] { getMethod }); - return func; - } + var value = genericGetter(instance); - private static PropertyInfo GetJoinFieldProperty(Type type) - { - var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public); - try - { - var joinField = properties.SingleOrDefault(p => p.PropertyType == typeof(JoinField)); - return joinField; - } - catch (InvalidOperationException e) - { - throw new ArgumentException($"{type.Name} has more than one JoinField property", e); - } + return (value is IFormattable formattable) + ? formattable.ToString(null, CultureInfo.InvariantCulture) + : value?.ToString(); } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/LazyJson.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/LazyJson.cs index 6a231c9a501..ad23b24088a 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/LazyJson.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/LazyJson.cs @@ -48,7 +48,10 @@ public override LazyJson Read(ref Utf8JsonReader reader, Type typeToConvert, Jso { InitializeSettings(options); + // TODO: fixme +#pragma warning disable IL2026, IL3050 using var jsonDoc = JsonSerializer.Deserialize(ref reader); +#pragma warning restore IL2026, IL3050 using var stream = _settings.MemoryStreamFactory.Create(); var writer = new Utf8JsonWriter(stream); diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamName.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamName.cs index f4e29f57e29..23ee30b7beb 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamName.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamName.cs @@ -7,7 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; - +using Elastic.Clients.Elasticsearch.Serialization; using Elastic.Transport; namespace Elastic.Clients.Elasticsearch; @@ -86,18 +86,16 @@ internal sealed class DataStreamNameConverter : JsonConverter { public override DataStreamName? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonTokenType.String) - throw new JsonException($"Unexpected token '{reader.TokenType}' for DataStreamName"); + reader.ValidateToken(JsonTokenType.String); return reader.GetString(); } public override void Write(Utf8JsonWriter writer, DataStreamName value, JsonSerializerOptions options) { - if (value is null || value.Name is null) + if (value?.Name is null) { - writer.WriteNullValue(); - return; + throw new ArgumentNullException(nameof(value)); } writer.WriteStringValue(value.Name); diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamNames.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamNames.cs index c5b71e481b2..e78b712c7ff 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamNames.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/DataStreamNames/DataStreamNames.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Elastic.Clients.Elasticsearch.Serialization; using Elastic.Transport; namespace Elastic.Clients.Elasticsearch; @@ -146,22 +147,20 @@ public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? prov internal sealed class DataStreamNamesConverter : JsonConverter { - public override DataStreamNames? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DataStreamNames Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonTokenType.StartArray) - throw new JsonException($"Expected {JsonTokenType.StartArray} token but read '{reader.TokenType}' token for DataStreamNames."); + var fields = reader.ReadCollectionValue(options, null)!; - return JsonSerializer.Deserialize(ref reader, options); + return new DataStreamNames(fields); } public override void Write(Utf8JsonWriter writer, DataStreamNames value, JsonSerializerOptions options) { if (value is null) { - writer.WriteNullValue(); - return; + throw new ArgumentNullException(nameof(value)); } - JsonSerializer.Serialize(writer, value.Names, options); + writer.WriteCollectionValue(options, value.Names, null); } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Name/Name.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Name/Name.cs index 844d550d1ca..f05de6d0373 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Name/Name.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Name/Name.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization; using Elastic.Clients.Elasticsearch.Serialization; @@ -13,7 +14,7 @@ namespace Elastic.Clients.Elasticsearch; [DebuggerDisplay("{DebugDisplay,nq}")] -[JsonConverter(typeof(StringAliasConverter))] +[JsonConverter(typeof(NameConverter))] public sealed class Name : IEquatable, IUrlParameter @@ -84,3 +85,42 @@ public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? prov #endregion IParsable } + +internal sealed class NameConverter : + JsonConverter +{ + public override Name? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + reader.ValidateToken(JsonTokenType.String); + + return reader.GetString()!; + } + + public override Name ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + reader.ValidateToken(JsonTokenType.PropertyName); + + return reader.GetString()!; + } + + public override void Write(Utf8JsonWriter writer, Name value, JsonSerializerOptions options) + { + if (value?.Value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + writer.WriteStringValue(value.Value); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, Name value, JsonSerializerOptions options) + { + if (value?.Value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + writer.WritePropertyName(value.Value); + } +} diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Username/Username.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Username/Username.cs index 11e106fb014..91ecae63213 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Username/Username.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/UrlParameters/Username/Username.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json; using System.Text.Json.Serialization; using Elastic.Clients.Elasticsearch.Serialization; @@ -13,7 +14,7 @@ namespace Elastic.Clients.Elasticsearch; [DebuggerDisplay("{DebugDisplay,nq}")] -[JsonConverter(typeof(StringAliasConverter))] +[JsonConverter(typeof(UsernameConverter))] public class Username : IEquatable, IUrlParameter @@ -76,3 +77,42 @@ public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? prov #endregion IParsable } + +internal sealed class UsernameConverter : + JsonConverter +{ + public override Name? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + reader.ValidateToken(JsonTokenType.String); + + return reader.GetString()!; + } + + public override Name ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + reader.ValidateToken(JsonTokenType.PropertyName); + + return reader.GetString()!; + } + + public override void Write(Utf8JsonWriter writer, Name value, JsonSerializerOptions options) + { + if (value?.Value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + writer.WriteStringValue(value.Value); + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, Name value, JsonSerializerOptions options) + { + if (value?.Value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + writer.WritePropertyName(value.Value); + } +} diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Helpers/BulkAllObservable.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Helpers/BulkAllObservable.cs index 6bd0b47669e..4f5871b67c0 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Helpers/BulkAllObservable.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Helpers/BulkAllObservable.cs @@ -221,11 +221,11 @@ private async Task HandleBulkRequestAsync(IList buffer, long { var clientException = response.ApiCallDetails.OriginalException as TransportException; var failureReason = clientException?.FailureReason; - var reason = failureReason?.GetStringValue() ?? nameof(PipelineFailure.BadRequest); + var reason = (failureReason is null) ? nameof(PipelineFailure.BadRequest) : EnumValue.GetString(failureReason.Value); switch (failureReason) { case PipelineFailure.MaxRetriesReached: - if (response.ApiCallDetails.AuditTrail.Last().Event == AuditEvent.FailedOverAllNodes) + if (response.ApiCallDetails.AuditTrail?.Last().Event == AuditEvent.FailedOverAllNodes) throw ThrowOnBadBulk(response, $"{nameof(BulkAll)} halted after attempted bulk failed over all the active nodes"); ThrowOnExhaustedRetries(); diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonReaderExtensions.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonReaderExtensions.cs index 20d03118d3a..dabde32cdf4 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonReaderExtensions.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonReaderExtensions.cs @@ -263,7 +263,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial /// the default converter for the item type . /// /// An instance of , or . - public static List? ReadCollectionValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, + public static List? ReadCollectionValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, JsonReadFunc? readElement) { if (reader.TokenType is JsonTokenType.Null) @@ -275,7 +275,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial readElement ??= static (ref Utf8JsonReader r, JsonSerializerOptions o) => ReadValue(ref r, o); - var result = new List(); + var result = new List(); while (reader.Read() && (reader.TokenType is not JsonTokenType.EndArray)) { @@ -302,7 +302,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial /// /// An instance of , or . /// If any dictionary key value is . - public static Dictionary? ReadDictionaryValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, + public static Dictionary? ReadDictionaryValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, JsonReadFunc? readKey, JsonReadFunc? readValue) where TKey : notnull { @@ -316,7 +316,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial readKey ??= static (ref Utf8JsonReader r, JsonSerializerOptions o) => ReadPropertyName(ref r, o); readValue ??= static (ref Utf8JsonReader r, JsonSerializerOptions o) => ReadValue(ref r, o); - var result = new Dictionary(); + var result = new Dictionary(); while (reader.Read() && (reader.TokenType is not JsonTokenType.EndObject)) { @@ -346,7 +346,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial /// converter for the type . /// /// An instance of . - public static KeyValuePair ReadKeyValuePairValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, + public static KeyValuePair ReadKeyValuePairValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, JsonReadFunc? readKey, JsonReadFunc? readValue) where TKey : notnull { @@ -360,7 +360,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial readKey ??= static (ref Utf8JsonReader r, JsonSerializerOptions o) => ReadPropertyName(ref r, o); readValue ??= static (ref Utf8JsonReader r, JsonSerializerOptions o) => ReadValue(ref r, o); - KeyValuePair result = default; + KeyValuePair result = default; while (reader.Read() && (reader.TokenType is not JsonTokenType.EndObject)) { var key = readKey(ref reader, options); @@ -421,7 +421,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial /// the default converter for the item type . /// /// An instance of , or . - public static List? ReadSingleOrManyCollectionValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, + public static List? ReadSingleOrManyCollectionValue(this ref Utf8JsonReader reader, JsonSerializerOptions options, JsonReadFunc? readElement) { if (reader.TokenType is JsonTokenType.Null) @@ -436,7 +436,7 @@ public static T ReadPropertyNameEx(this ref Utf8JsonReader reader, JsonSerial return [readElement(ref reader, options)]; } - var result = new List(); + var result = new List(); while (reader.Read() && (reader.TokenType is not JsonTokenType.EndArray)) { diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonSerializerOptionsExtensions.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonSerializerOptionsExtensions.cs index a32fcc29962..d161c05b17f 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonSerializerOptionsExtensions.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Next/JsonSerializerOptionsExtensions.cs @@ -20,7 +20,9 @@ public static JsonConverter GetConverter(this JsonSerializerOptions option // to directly use converters like we do. // When getting a default generic converter from `JsonSerializerOptions` that are not read-only, a // `NotSupportedException` is thrown as soon as we call `converter.Read()` or `converter.Write()`. +#pragma warning disable IL2026, IL3050 options.MakeReadOnly(true); +#pragma warning restore IL2026, IL3050 #pragma warning disable IL2026, IL3050 var rawConverter = options.GetConverter(markerType ?? typeof(T)); diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultRequestResponseSerializer.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultRequestResponseSerializer.cs index bef7a01b64b..c08aa351e96 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultRequestResponseSerializer.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultRequestResponseSerializer.cs @@ -12,16 +12,22 @@ using System.Threading.Tasks; using Elastic.Transport; +using Elastic.Transport.Products.Elasticsearch; namespace Elastic.Clients.Elasticsearch.Serialization; /// /// The built-in internal serializer that the uses to serialize built in types. /// -internal sealed class DefaultRequestResponseSerializer : SystemTextJsonSerializer +internal sealed class DefaultRequestResponseSerializer : + SystemTextJsonSerializer { private readonly IElasticsearchClientSettings _settings; + /// + /// Constructs a new instance. + /// + /// The instance to which this serializer will be linked. public DefaultRequestResponseSerializer(IElasticsearchClientSettings settings) : base(new DefaultRequestResponseSerializerOptionsProvider(settings)) { @@ -70,7 +76,7 @@ public override T Deserialize(Stream stream) return base.Deserialize(type, stream); } - public override ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) + public override ValueTask DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) { if (typeof(IStreamSerializable).IsAssignableFrom(typeof(T))) { @@ -79,7 +85,7 @@ public override T Deserialize(Stream stream) return base.DeserializeAsync(stream, cancellationToken); } - public override ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = new CancellationToken()) + public override ValueTask DeserializeAsync(Type type, Stream stream, CancellationToken cancellationToken = default) { if (typeof(IStreamSerializable).IsAssignableFrom(type)) { @@ -137,10 +143,57 @@ private static IReadOnlyCollection CreateDefaultBuiltInConverters private static void MutateOptions(JsonSerializerOptions options) { - options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); +#pragma warning disable IL2026, IL3050 + options.TypeInfoResolver = JsonTypeInfoResolver.Combine( + RequestResponseSerializerContext.Default, + ElasticsearchTransportSerializerContext.Default, + new DefaultJsonTypeInfoResolver() + ); +#pragma warning restore IL2026, IL3050 + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; options.IncludeFields = true; options.NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals; options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; } } + +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(bool?))] +[JsonSerializable(typeof(byte))] +[JsonSerializable(typeof(byte?))] +[JsonSerializable(typeof(sbyte))] +[JsonSerializable(typeof(sbyte?))] +[JsonSerializable(typeof(char))] +[JsonSerializable(typeof(char?))] +[JsonSerializable(typeof(decimal))] +[JsonSerializable(typeof(decimal?))] +[JsonSerializable(typeof(double))] +[JsonSerializable(typeof(double?))] +[JsonSerializable(typeof(float))] +[JsonSerializable(typeof(float?))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(int?))] +[JsonSerializable(typeof(uint))] +[JsonSerializable(typeof(uint?))] +[JsonSerializable(typeof(nint))] +[JsonSerializable(typeof(nint?))] +[JsonSerializable(typeof(nuint))] +[JsonSerializable(typeof(nuint?))] +[JsonSerializable(typeof(long))] +[JsonSerializable(typeof(long?))] +[JsonSerializable(typeof(ulong))] +[JsonSerializable(typeof(ulong?))] +[JsonSerializable(typeof(short))] +[JsonSerializable(typeof(short?))] +[JsonSerializable(typeof(ushort))] +[JsonSerializable(typeof(ushort?))] +[JsonSerializable(typeof(object))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(DateTimeOffset))] +[JsonSerializable(typeof(TimeSpan))] +internal sealed partial class RequestResponseSerializerContext : + JsonSerializerContext +{ +} diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultSourceSerializer.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultSourceSerializer.cs index 60418a4afe6..bff5642ef93 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultSourceSerializer.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/DefaultSourceSerializer.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; + using Elastic.Transport; namespace Elastic.Clients.Elasticsearch.Serialization; @@ -19,53 +20,82 @@ public class DefaultSourceSerializer : SystemTextJsonSerializer { /// - /// Constructs a new instance that accepts an that can - /// be provided to customize the default . + /// Constructs a new instance. /// - /// An instance to which this serializer - /// will be linked. - /// - /// - /// An optional to customize the configuration of the default . - /// + /// The instance to which this serializer will be linked. + /// An optional to customize the default . public DefaultSourceSerializer(IElasticsearchClientSettings settings, Action? configureOptions = null) : base(new DefaultSourceSerializerOptionsProvider(settings, configureOptions)) { } + + /// + /// Constructs a new instance. + /// + /// The instance to which this serializer will be linked. + /// A custom to use. + /// An optional to customize the default . + public DefaultSourceSerializer(IElasticsearchClientSettings settings, IJsonTypeInfoResolver typeInfoResolver, Action? configureOptions = null) : + base(new DefaultSourceSerializerOptionsProvider(settings, typeInfoResolver, configureOptions)) + { + } } /// -/// The options-provider for the built-in . +/// The default implementation for the built-in . /// -public class DefaultSourceSerializerOptionsProvider : +internal sealed class DefaultSourceSerializerOptionsProvider : TransportSerializerOptionsProvider { + /// + /// Constructs a new instance. + /// + /// The instance to which this serializer options will be linked. + /// An optional to customize the default . public DefaultSourceSerializerOptionsProvider(IElasticsearchClientSettings settings, Action? configureOptions = null) : - base(CreateDefaultBuiltInConverters(settings), null, options => MutateOptions(options, configureOptions)) + base( + CreateDefaultBuiltInConverters(settings), + null, + options => MutateOptions(options, null, configureOptions) + ) { } - public DefaultSourceSerializerOptionsProvider(IElasticsearchClientSettings settings, bool registerDefaultConverters, Action? configureOptions = null) : - base(registerDefaultConverters ? CreateDefaultBuiltInConverters(settings) : [], null, options => MutateOptions(options, configureOptions)) + /// + /// Constructs a new instance. + /// + /// The instance to which this serializer options will be linked. + /// A custom to use. + /// An optional to customize the default . + public DefaultSourceSerializerOptionsProvider(IElasticsearchClientSettings settings, IJsonTypeInfoResolver typeInfoResolver, Action? configureOptions = null) : + base( + CreateDefaultBuiltInConverters(settings), + null, + options => MutateOptions(options, typeInfoResolver ?? throw new ArgumentNullException(nameof(typeInfoResolver)), configureOptions) + ) { } /// - /// Returns an array of the built-in s that are used registered with the source serializer by default. + /// Returns an array of the built-in s that are registered with the source serializer by default. /// private static IReadOnlyCollection CreateDefaultBuiltInConverters(IElasticsearchClientSettings settings) => [ // For context aware JsonConverter/JsonConverterFactory implementations. new ContextProvider(settings), +#pragma warning disable IL3050 new JsonStringEnumConverter(), +#pragma warning restore IL3050 new DoubleWithFractionalPortionConverter(), new SingleWithFractionalPortionConverter() ]; - private static void MutateOptions(JsonSerializerOptions options, Action? configureOptions) + private static void MutateOptions(JsonSerializerOptions options, IJsonTypeInfoResolver? typeInfoResolver, Action? configureOptions) { - options.TypeInfoResolver = new DefaultJsonTypeInfoResolver(); +#pragma warning disable IL2026, IL3050 + options.TypeInfoResolver = typeInfoResolver ?? new DefaultJsonTypeInfoResolver(); +#pragma warning restore IL2026, IL3050 options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/StringAliasConverter.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/StringAliasConverter.cs deleted file mode 100644 index a1107687c1a..00000000000 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Serialization/StringAliasConverter.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using System; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Elastic.Clients.Elasticsearch.Serialization; - -// TODO - In .NET 7 we could review supporting IParsable as a type constraint? -internal sealed class StringAliasConverter : JsonConverter -{ - public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => Read(ref reader, typeToConvert, options); - - public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => writer.WritePropertyName(value.ToString()); - - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - - var instance = (T)Activator.CreateInstance( - typeof(T), - BindingFlags.Instance | BindingFlags.Public, - args: new object[] { value }, - binder: null, - culture: null)!; - - return instance; - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - if (value is null) - { - writer.WriteNullValue(); - } - else - { - writer.WriteStringValue(value.ToString()); - } - } -} diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/BucketsPath.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/BucketsPath.cs index 21c585824f1..2a68eb2bf8d 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/BucketsPath.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/BucketsPath.cs @@ -11,6 +11,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; +using Elastic.Clients.Elasticsearch.Serialization; namespace Elastic.Clients.Elasticsearch.Aggregations; @@ -97,15 +98,17 @@ public bool TryGetDictionary([NotNullWhen(true)] out Dictionary? internal sealed class BucketsPathConverter : JsonConverter { - public override BucketsPath? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => - (reader.TokenType) switch + public override BucketsPath? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return (reader.TokenType) switch { JsonTokenType.Null => null, - JsonTokenType.String => BucketsPath.Single(JsonSerializer.Deserialize(ref reader, options)!), - JsonTokenType.StartArray => BucketsPath.Array(JsonSerializer.Deserialize(ref reader, options)!), - JsonTokenType.StartObject => BucketsPath.Dictionary(JsonSerializer.Deserialize>(ref reader, options)!), + JsonTokenType.String => BucketsPath.Single(reader.ReadValue(options)!), + JsonTokenType.StartArray => BucketsPath.Array(reader.ReadCollectionValue(options, null)!.ToArray()), + JsonTokenType.StartObject => BucketsPath.Dictionary(reader.ReadDictionaryValue(options, null, null)!), _ => throw new JsonException($"Unexpected token '{reader.TokenType}'.") }; + } public override void Write(Utf8JsonWriter writer, BucketsPath value, JsonSerializerOptions options) { @@ -116,11 +119,11 @@ public override void Write(Utf8JsonWriter writer, BucketsPath value, JsonSeriali break; case BucketsPath.Kind.Array: - JsonSerializer.Serialize(writer, (string[])value._value, options); + writer.WriteCollectionValue(options, (string[])value._value, null); break; case BucketsPath.Kind.Dictionary: - JsonSerializer.Serialize(writer, (Dictionary)value._value, options); + writer.WriteDictionaryValue(options, (Dictionary)value._value, null, null); break; default: diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsExclude.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsExclude.cs index 67d4491be29..f6dd968f7d1 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsExclude.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsExclude.cs @@ -7,7 +7,10 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Elastic.Clients.Elasticsearch.Serialization; + #nullable enable + namespace Elastic.Clients.Elasticsearch.Aggregations; /// @@ -55,44 +58,27 @@ public TermsExclude(IEnumerable values) internal sealed class TermsExcludeConverter : JsonConverter { - public override TermsExclude? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override TermsExclude Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) + return reader.TokenType switch { - reader.Read(); - return null; - } - - TermsExclude termsExclude; - - switch (reader.TokenType) - { - case JsonTokenType.StartArray: - var terms = JsonSerializer.Deserialize(ref reader, options) ?? Array.Empty(); - termsExclude = new TermsExclude(terms); - break; - case JsonTokenType.String: - var regex = reader.GetString(); - termsExclude = new TermsExclude(regex!); - break; - default: - throw new JsonException($"Unexpected token {reader.TokenType} when deserializing {nameof(TermsExclude)}"); - } - - return termsExclude; + JsonTokenType.StartArray => new TermsExclude(reader.ReadCollectionValue(options, null)!), + JsonTokenType.String => new TermsExclude(reader.ReadValue(options)!), + _ => throw new JsonException( + $"Unexpected token {reader.TokenType} when deserializing {nameof(TermsExclude)}") + }; } public override void Write(Utf8JsonWriter writer, TermsExclude value, JsonSerializerOptions options) { if (value is null) { - writer.WriteNullValue(); - return; + throw new ArgumentNullException(nameof(value)); } if (value.Values is not null) { - JsonSerializer.Serialize(writer, value.Values, options); + writer.WriteCollectionValue(options, value.Values, null); return; } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsInclude.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsInclude.cs index 2861d59703c..eac0bbdb519 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsInclude.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Aggregations/TermsInclude.cs @@ -7,7 +7,10 @@ using System.Text.Json; using System.Text.Json.Serialization; +using Elastic.Clients.Elasticsearch.Serialization; + #nullable enable + namespace Elastic.Clients.Elasticsearch.Aggregations; /// @@ -79,24 +82,15 @@ internal sealed class TermsIncludeConverter : JsonConverter { public override TermsInclude? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType == JsonTokenType.Null) - { - reader.Read(); - return null; - } - - TermsInclude termsInclude; - switch (reader.TokenType) { case JsonTokenType.StartArray: - var terms = JsonSerializer.Deserialize(ref reader, options) ?? Array.Empty(); - termsInclude = new TermsInclude(terms); - break; + return new TermsInclude(reader.ReadCollectionValue(options, null)!); + case JsonTokenType.StartObject: long partition = 0; long numberOfPartitions = 0; - while(reader.Read() && reader.TokenType != JsonTokenType.EndObject) + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) { if (reader.TokenType == JsonTokenType.PropertyName) { @@ -107,38 +101,36 @@ internal sealed class TermsIncludeConverter : JsonConverter case "partition": partition = reader.GetInt64(); break; + case "num_partitions": numberOfPartitions = reader.GetInt64(); break; + default: throw new JsonException($"Unexpected property name '{propertyName}' encountered when deserializing TermsInclude."); } } } - termsInclude = new TermsInclude(partition, numberOfPartitions); - break; + return new TermsInclude(partition, numberOfPartitions); + case JsonTokenType.String: - var regex = reader.GetString(); - termsInclude = new TermsInclude(regex!); - break; + return new TermsInclude(reader.ReadValue(options)!); + default: throw new JsonException($"Unexpected token {reader.TokenType} when deserializing {nameof(TermsInclude)}"); } - - return termsInclude; } public override void Write(Utf8JsonWriter writer, TermsInclude value, JsonSerializerOptions options) { if (value is null) { - writer.WriteNullValue(); - return; + throw new ArgumentNullException(nameof(value)); } if (value.Values is not null) { - JsonSerializer.Serialize(writer, value.Values, options); + writer.WriteCollectionValue(options, value.Values, null); return; } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/BulkUpdateBody.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/BulkUpdateBody.cs index 0b978370798..0f2324ccd87 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/BulkUpdateBody.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/BulkUpdateBody.cs @@ -62,23 +62,9 @@ protected override void SerializeProperties(Utf8JsonWriter writer, JsonSerialize settings.SourceSerializer.Serialize(PartialUpdate, writer); } - if (Script is not null) - { - writer.WritePropertyName("script"); - JsonSerializer.Serialize(writer, Script, options); - } - - if (ScriptedUpsert.HasValue) - { - writer.WritePropertyName("scripted_upsert"); - JsonSerializer.Serialize(writer, ScriptedUpsert.Value, options); - } - - if (DocAsUpsert.HasValue) - { - writer.WritePropertyName("doc_as_upsert"); - JsonSerializer.Serialize(writer, DocAsUpsert.Value, options); - } + writer.WriteProperty(options, "script", Script); + writer.WriteProperty(options, "scripted_upsert", ScriptedUpsert); + writer.WriteProperty(options, "doc_as_upsert", DocAsUpsert); if (Upsert is not null) { @@ -92,11 +78,13 @@ protected override void SerializeProperties(Utf8JsonWriter writer, JsonSerialize switch (Source.Tag) { case UnionTag.T1: - JsonSerializer.Serialize(writer, Source.Value1, options); + writer.WriteValue(options, Source.Value1); break; + case UnionTag.T2: - JsonSerializer.Serialize(writer, Source.Value2, options); + writer.WriteValue(options, Source.Value2); break; + default: throw new InvalidOperationException(); } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/ScriptedBulkUpdateBody.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/ScriptedBulkUpdateBody.cs index ead19886ca3..a7c30a0a1c7 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/ScriptedBulkUpdateBody.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Core/Bulk/ScriptedBulkUpdateBody.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System.Text.Json; + +using Elastic.Clients.Elasticsearch.Serialization; using Elastic.Transport.Extensions; namespace Elastic.Clients.Elasticsearch.Core.Bulk; @@ -13,11 +15,7 @@ internal class ScriptedBulkUpdateBody : BulkUpdateBody protected override void SerializeProperties(Utf8JsonWriter writer, JsonSerializerOptions options, IElasticsearchClientSettings settings) { - if (Script is not null) - { - writer.WritePropertyName("script"); - JsonSerializer.Serialize(writer, Script, options); - } + writer.WriteProperty(options, "script", Script); } } @@ -27,11 +25,7 @@ internal class ScriptedBulkUpdateBody : ScriptedBulkUpdateBody protected override void SerializeProperties(Utf8JsonWriter writer, JsonSerializerOptions options, IElasticsearchClientSettings settings) { - if (Script is not null) - { - writer.WritePropertyName("script"); - JsonSerializer.Serialize(writer, Script, options); - } + writer.WriteProperty(options, "script", Script); if (Upsert is not null) { diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Mapping/Properties.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Mapping/Properties.cs index 6b0ebb9d460..9e3d6f12da9 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Mapping/Properties.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Mapping/Properties.cs @@ -41,7 +41,7 @@ internal sealed class PropertiesConverter : JsonConverter var propertyName = reader.GetString(); reader.Read(); - var property = JsonSerializer.Deserialize(ref reader, options); + var property = reader.ReadValue(options); properties.Add(propertyName, property); } @@ -71,7 +71,7 @@ public override void Write(Utf8JsonWriter writer, Properties value, JsonSerializ continue; } - JsonSerializer.Serialize(writer, properties.BackingDictionary, options); + writer.WriteDictionaryValue(options, properties.BackingDictionary, null, null); } } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Slices.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Slices.cs index 2d0b460f4ff..413a50604d4 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Slices.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Slices.cs @@ -42,7 +42,7 @@ public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? prov return true; } - if (EnumValueParser.TryParse(s, out var computed)) + if (EnumValue.TryParse(s, out var computed)) { result = new Slices(computed); return true; diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlRow.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlRow.cs index d002fd031d9..af039b96505 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlRow.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlRow.cs @@ -7,6 +7,7 @@ using System.Collections.ObjectModel; using System.Text.Json; using System.Text.Json.Serialization; +using Elastic.Clients.Elasticsearch.Serialization; namespace Elastic.Clients.Elasticsearch.Sql; @@ -32,7 +33,7 @@ internal sealed class SqlRowConverter : JsonConverter while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { - var value = JsonSerializer.Deserialize(ref reader, options); + var value = reader.ReadValue(options); values.Add(value); } diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlValue.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlValue.cs index edd5d94d6c5..2c6d0801646 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlValue.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Types/Sql/SqlValue.cs @@ -5,6 +5,7 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; +using Elastic.Clients.Elasticsearch.Serialization; namespace Elastic.Clients.Elasticsearch.Sql; @@ -28,7 +29,7 @@ public override SqlValue Read(ref Utf8JsonReader reader, Type typeToConvert, Jso return default; } - var lazyDoc = JsonSerializer.Deserialize(ref reader, options); + var lazyDoc = reader.ReadValue(options); return new SqlValue(lazyDoc); } diff --git a/src/Playground/Playground.csproj b/src/Playground/Playground.csproj index eace8526b9c..7028e3859b3 100644 --- a/src/Playground/Playground.csproj +++ b/src/Playground/Playground.csproj @@ -1,4 +1,4 @@ - + Exe @@ -7,10 +7,13 @@ enable + + true + false + + - - - +