Skip to content

Commit 5aa2409

Browse files
Fix resolvable dictionary properties (#7076) (#7078)
* Prefer IReadOnlyDictionary on read only (response) types * Introduce readonly dictionary converters for Field and IndexName These ensure keys are santised during lookups so that items can be found correctly using inferred values. * Prefer IDictionary for properties on writable types * Update tests * Fix BOM * Cleanup IUrlParameter types for consistency Co-authored-by: Steve Gordon <[email protected]>
1 parent 26e345e commit 5aa2409

File tree

251 files changed

+994
-800
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

251 files changed

+994
-800
lines changed

src/Elastic.Clients.Elasticsearch/Api/IndexManagement/GetFieldMappingResponse.cs

+78
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,91 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
using System;
56
using System.Collections.Generic;
7+
using System.Linq.Expressions;
68
using System.Text.Json.Serialization;
9+
using Elastic.Clients.Elasticsearch.Mapping;
710

811
namespace Elastic.Clients.Elasticsearch.IndexManagement;
912

1013
public partial class GetFieldMappingResponse
1114
{
1215
[JsonIgnore]
1316
public IReadOnlyDictionary<IndexName, TypeFieldMappings> FieldMappings => BackingDictionary;
17+
18+
public IProperty? GetProperty(IndexName index, Field field)
19+
{
20+
if (index is null)
21+
throw new ArgumentNullException(nameof(index));
22+
23+
if (field is null)
24+
throw new ArgumentNullException(nameof(field));
25+
26+
var mappings = MappingsFor(index);
27+
28+
if (mappings is null)
29+
return null;
30+
31+
if (!mappings.TryGetValue(field, out var fieldMapping) || fieldMapping.Mapping is null)
32+
return null;
33+
34+
return fieldMapping.Mapping.TryGetProperty(PropertyName.FromField(field), out var property) ? property : null;
35+
}
36+
37+
public bool TryGetProperty(IndexName index, Field field, out IProperty property)
38+
{
39+
property = null;
40+
41+
if (index is null)
42+
throw new ArgumentNullException(nameof(index));
43+
44+
if (field is null)
45+
throw new ArgumentNullException(nameof(field));
46+
47+
var mappings = MappingsFor(index);
48+
49+
if (mappings is null)
50+
return false;
51+
52+
if (!mappings.TryGetValue(field, out var fieldMapping) || fieldMapping.Mapping is null)
53+
return false;
54+
55+
if (fieldMapping.Mapping.TryGetProperty(PropertyName.FromField(field), out var matched))
56+
{
57+
property = matched;
58+
return true;
59+
}
60+
61+
return false;
62+
}
63+
64+
public IProperty? PropertyFor<T>(Field field) => PropertyFor<T>(field, null);
65+
66+
public IProperty? PropertyFor<T>(Field field, IndexName index) =>
67+
GetProperty(index ?? Infer.Index<T>(), field);
68+
69+
public IProperty? PropertyFor<T, TValue>(Expression<Func<T, TValue>> objectPath)
70+
where T : class =>
71+
GetProperty(Infer.Index<T>(), Infer.Field(objectPath));
72+
73+
public IProperty? PropertyFor<T, TValue>(Expression<Func<T, TValue>> objectPath, IndexName index)
74+
where T : class =>
75+
GetProperty(index, Infer.Field(objectPath));
76+
77+
public IProperty? PropertyFor<T>(Expression<Func<T, object>> objectPath)
78+
where T : class =>
79+
GetProperty(Infer.Index<T>(), Infer.Field(objectPath));
80+
81+
public IProperty? PropertyFor<T>(Expression<Func<T, object>> objectPath, IndexName index)
82+
where T : class =>
83+
GetProperty(index, Infer.Field(objectPath));
84+
85+
private IReadOnlyDictionary<Field, FieldMapping> MappingsFor(IndexName index)
86+
{
87+
if (!FieldMappings.TryGetValue(index, out var indexMapping) || indexMapping.Mappings == null)
88+
return null;
89+
90+
return indexMapping.Mappings;
91+
}
1492
}

src/Elastic.Clients.Elasticsearch/Core/Infer/IndexName/IndexName.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ private IndexName(string index, Type type, string cluster = null)
4545

4646
bool IEquatable<IndexName>.Equals(IndexName other) => EqualsMarker(other);
4747

48-
public string GetString(ITransportConfiguration settings)
48+
string IUrlParameter.GetString(ITransportConfiguration settings)
4949
{
5050
if (settings is not IElasticsearchClientSettings elasticsearchClientSettings)
5151
throw new Exception("Tried to pass index name on querystring but it could not be resolved because no Elastic.Clients.Elasticsearch settings are available.");

src/Elastic.Clients.Elasticsearch/Core/Infer/Metric/Metrics.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
namespace Elastic.Clients.Elasticsearch;
99

10-
public class Metrics : IEquatable<Metrics>, IUrlParameter
10+
public sealed class Metrics : IEquatable<Metrics>, IUrlParameter
1111
{
1212
// TODO: Complete this
1313

@@ -27,7 +27,7 @@ public class Metrics : IEquatable<Metrics>, IUrlParameter
2727

2828
public bool Equals(Metrics other) => Value.Equals(other.Value);
2929

30-
public string GetString(ITransportConfiguration settings) => string.Empty; // TODO Value.GetStringValue();
30+
string IUrlParameter.GetString(ITransportConfiguration settings) => string.Empty; // TODO Value.GetStringValue();
3131

3232
//public static implicit operator Metrics(IndicesStatsMetric metric) => new Metrics(metric);
3333

src/Elastic.Clients.Elasticsearch/Core/Infer/PropertyName/PropertyName.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public PropertyName(PropertyInfo property)
3939
_comparisonValue = property;
4040
_type = property.DeclaringType;
4141
}
42-
42+
4343
public bool CacheableExpression { get; }
4444
public Expression Expression { get; }
4545

@@ -78,6 +78,20 @@ public static implicit operator PropertyName(Expression expression) =>
7878
public static implicit operator PropertyName(PropertyInfo property) =>
7979
property == null ? null : new PropertyName(property);
8080

81+
internal static PropertyName FromField(Field field)
82+
{
83+
if (field.Property is not null)
84+
return new PropertyName(field.Property);
85+
86+
if (field.Expression is not null)
87+
return new PropertyName(field.Expression);
88+
89+
if (field.Name is not null)
90+
return new PropertyName(field.Name);
91+
92+
return null;
93+
}
94+
8195
public override int GetHashCode()
8296
{
8397
unchecked

src/Elastic.Clients.Elasticsearch/Core/Infer/Timestamp/Timestamp.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ public sealed class Timestamp : IUrlParameter, IEquatable<Timestamp>
1616

1717
public bool Equals(Timestamp other) => Value == other.Value;
1818

19-
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
20-
public string GetString(ITransportConfiguration settings) => Value.ToString(CultureInfo.InvariantCulture);
19+
string IUrlParameter.GetString(ITransportConfiguration settings) => GetString();
20+
21+
public override string ToString() => GetString();
22+
23+
private string GetString() => Value.ToString(CultureInfo.InvariantCulture);
2124

2225
public static implicit operator Timestamp(DateTimeOffset categoryId) => new(categoryId.ToUnixTimeMilliseconds());
2326

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using Elastic.Transport;
8+
9+
namespace Elastic.Clients.Elasticsearch;
10+
11+
/// <summary>
12+
/// A specialised readonly dictionary for <typeparamref name="TValue"/> data, keyed by <see cref="IndexName"/>.
13+
/// <para>This supports inferrence enabled lookups by ensuring keys are sanitized when storing the values and when performing lookups.</para>
14+
/// </summary>
15+
/// <typeparam name="TValue"></typeparam>
16+
internal readonly struct ReadOnlyFieldDictionary<TValue> : IReadOnlyDictionary<Field, TValue>
17+
{
18+
private readonly Dictionary<Field, TValue> _backingDictionary;
19+
private readonly IElasticsearchClientSettings? _settings;
20+
21+
public ReadOnlyFieldDictionary()
22+
{
23+
_backingDictionary = new Dictionary<Field, TValue>(0);
24+
_settings = null;
25+
}
26+
27+
internal ReadOnlyFieldDictionary(Dictionary<Field, TValue> source, IElasticsearchClientSettings settings)
28+
{
29+
_settings = settings;
30+
31+
// This is an "optimised version" which doesn't cause a second dictionary to be allocated.
32+
// Since we expect this to be used only for deserialisation, the keys received will already have been strings,
33+
// so no further sanitisation is required.
34+
35+
if (source == null)
36+
{
37+
_backingDictionary = new Dictionary<Field, TValue>(0);
38+
return;
39+
}
40+
41+
_backingDictionary = source;
42+
}
43+
44+
private string Sanitize(Field key) => _settings is not null ? (key as IUrlParameter)?.GetString(_settings) : string.Empty;
45+
46+
public TValue this[Field key] => _backingDictionary.TryGetValue(Sanitize(key), out var v) ? v : default;
47+
public TValue this[string key] => _backingDictionary.TryGetValue(key, out var v) ? v : default;
48+
49+
public IEnumerable<Field> Keys => _backingDictionary.Keys;
50+
public IEnumerable<TValue> Values => _backingDictionary.Values;
51+
public int Count => _backingDictionary.Count;
52+
public bool ContainsKey(Field key) => _backingDictionary.ContainsKey(Sanitize(key));
53+
public IEnumerator<KeyValuePair<Field, TValue>> GetEnumerator() => _backingDictionary.GetEnumerator();
54+
public bool TryGetValue(Field key, out TValue value) => _backingDictionary.TryGetValue(Sanitize(key), out value);
55+
IEnumerator IEnumerable.GetEnumerator() => _backingDictionary.GetEnumerator();
56+
}

src/Elastic.Clients.Elasticsearch/Core/ReadOnlyIndexNameDictionary.cs

+3-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Collections;
66
using System.Collections.Generic;
7+
using Elastic.Transport;
78

89
namespace Elastic.Clients.Elasticsearch;
910

@@ -12,7 +13,7 @@ namespace Elastic.Clients.Elasticsearch;
1213
/// <para>This supports inferrence enabled lookups by ensuring keys are sanitized when storing the values and when performing lookups.</para>
1314
/// </summary>
1415
/// <typeparam name="TValue"></typeparam>
15-
public struct ReadOnlyIndexNameDictionary<TValue> : IReadOnlyDictionary<IndexName, TValue>
16+
internal readonly struct ReadOnlyIndexNameDictionary<TValue> : IReadOnlyDictionary<IndexName, TValue>
1617
{
1718
private readonly Dictionary<IndexName, TValue> _backingDictionary;
1819
private readonly IElasticsearchClientSettings? _settings;
@@ -31,21 +32,16 @@ internal ReadOnlyIndexNameDictionary(Dictionary<IndexName, TValue> source, IElas
3132
// Since we expect this to be used only for deserialisation, the keys received will already have been strings,
3233
// so no further sanitisation is required.
3334

34-
//var backingDictionary = new Dictionary<IndexName, TValue>(source.Count);
35-
3635
if (source == null)
3736
{
3837
_backingDictionary = new Dictionary<IndexName, TValue>(0);
3938
return;
4039
}
4140

42-
//foreach (var key in source.Keys)
43-
// backingDictionary[Sanitize(key)] = source[key];
44-
4541
_backingDictionary = source;
4642
}
4743

48-
private string Sanitize(IndexName key) => _settings is not null ? key?.GetString(_settings) : string.Empty;
44+
private string Sanitize(IndexName key) => _settings is not null ? (key as IUrlParameter)?.GetString(_settings) : string.Empty;
4945

5046
public TValue this[IndexName key] => _backingDictionary.TryGetValue(Sanitize(key), out var v) ? v : default;
5147
public TValue this[string key] => _backingDictionary.TryGetValue(key, out var v) ? v : default;

src/Elastic.Clients.Elasticsearch/Core/UrlParameters/IndexUuid/IndexUuid.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ public bool Equals(IndexUuid other)
2626
return false;
2727
}
2828

29-
public string GetString(ITransportConfiguration settings) => Value;
29+
string IUrlParameter.GetString(ITransportConfiguration settings) => Value;
30+
31+
public override string ToString() => Value;
3032

3133
public override bool Equals(object obj)
3234
{

src/Elastic.Clients.Elasticsearch/Core/UrlParameters/TaskId/TaskId.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System;
66
using System.Diagnostics;
77
using System.Globalization;
8-
using System.Runtime;
98
using System.Text.Json;
109
using System.Text.Json.Serialization;
1110
using Elastic.Clients.Elasticsearch.Serialization;
@@ -47,7 +46,7 @@ public TaskId(string taskId)
4746

4847
public bool Equals(TaskId other) => EqualsString(other?.FullyQualifiedId);
4948

50-
public string GetString(ITransportConfiguration settings) => FullyQualifiedId;
49+
string IUrlParameter.GetString(ITransportConfiguration settings) => FullyQualifiedId;
5150

5251
public override string ToString() => FullyQualifiedId;
5352

@@ -77,7 +76,7 @@ public override void WriteAsPropertyName(Utf8JsonWriter writer, TaskId value, Js
7776
{
7877
if (options.TryGetClientSettings(out var settings))
7978
{
80-
writer.WritePropertyName(value.GetString(settings));
79+
writer.WritePropertyName(((IUrlParameter)value).GetString(settings));
8180
return;
8281
}
8382

src/Elastic.Clients.Elasticsearch/Serialization/DefaultRequestResponseSerializer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public DefaultRequestResponseSerializer(IElasticsearchClientSettings settings)
2929
{
3030
new KeyValuePairConverterFactory(settings),
3131
new SourceConverterFactory(settings),
32-
new ReadOnlyIndexNameDictionaryConverterFactory(settings),
3332
new CalendarIntervalConverter(),
3433
new IndexNameConverter(settings),
3534
new ObjectToInferredTypesConverter(),
@@ -47,6 +46,7 @@ public DefaultRequestResponseSerializer(IElasticsearchClientSettings settings)
4746
new IndicesJsonConverter(settings),
4847
new IdsConverter(settings),
4948
new IsADictionaryConverterFactory(),
49+
//new ResolvableReadonlyDictionaryConverterFactory(settings),
5050
new ResponseItemConverterFactory(),
5151
new UnionConverter(),
5252
new ExtraSerializationData(settings),

src/Elastic.Clients.Elasticsearch/Serialization/InterfaceConverterFactory.cs

-40
This file was deleted.

src/Elastic.Clients.Elasticsearch/Serialization/IntermediateConverter.cs

-27
This file was deleted.

0 commit comments

Comments
 (0)