Skip to content

Commit 26583e6

Browse files
committed
Honour DataMember and DataContract attributes (#3304)
* Honour DataMember and DataContract attributes This commit updates the behaviour of TypeNameResolver and PropertyMapping to respect the usage of DataMember and DataContract attributes for all requests. Prior behaviour to this commit was that indexing would honour DataMember attributes on a document due to the internal implementation of JSON.Net honouring them. With other requests such as Search however, where a Field type is used, the resolution of the Field string name did not honour the attributes. Include behaviour in documentation Closes #3107 (cherry picked from commit 97455b8)
1 parent 5bdb99f commit 26583e6

File tree

10 files changed

+354
-76
lines changed

10 files changed

+354
-76
lines changed

docs/client-concepts/high-level/inference/field-inference.asciidoc

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ Expect("name")
116116
To ease creating a `Field` instance from expressions, there is a static `Infer` class you can use
117117

118118
[TIP]
119-
This example uses the https://msdn.microsoft.com/en-us/library/sf0df423.aspx#Anchor_0[static import] `using static Nest.Infer;` in the using directives to shorthand `Nest.Infer.Field<T>()`
119+
This example uses the https://msdn.microsoft.com/en-us/library/sf0df423.aspx#Anchor_0[static import] `using static Nest.Infer;`
120+
in the using directives to shorthand `Nest.Infer.Field<T>()`
120121
to simply `Field<T>()`. Be sure to include this static import if copying any of these examples.
121122

122123
[source,csharp]
@@ -324,6 +325,7 @@ Expect("leadDeveloper.firstName.raw.evendeeper").WhenSerializing(multiSuffixFiel
324325
Expect("metadata.hardcoded.raw.evendeeper").WhenSerializing(multiSuffixFieldExpressions[4]);
325326
----
326327

328+
[[field-name-attribute]]
327329
==== Attribute based naming
328330

329331
Using NEST's property attributes you can specify a new name for the properties
@@ -339,9 +341,29 @@ public class BuiltIn
339341
Expect("naam").WhenSerializing(Field<BuiltIn>(p => p.Name));
340342
----
341343

342-
Starting with NEST 2.x, we also ask the serializer if it can resolve a property to a name.
343-
Here we ask the default `JsonNetSerializer` to resolve a property name and it takes
344-
the `JsonPropertyAttribute` into account
344+
[[data-member-field-attribute]]
345+
==== DataMember attributes
346+
347+
If a property has a `System.Runtime.Serialization.DataMemberAttribute` applied, this can be used to resolve
348+
a field value for a property
349+
350+
[source,csharp]
351+
----
352+
public class DataMember
353+
{
354+
[DataMember(Name = "nameFromDataMember")]
355+
public string Name { get; set; }
356+
}
357+
358+
Expect("nameFromDataMember").WhenSerializing(Field<DataMember>(p => p.Name));
359+
----
360+
361+
[[serializer-specific-field-attribute]]
362+
==== Serializer specific attributes
363+
364+
NEST can also use a serializer specific attribute to resolve a field value for a property.
365+
In this example, we use the `JsonPropertyAttribute` to resolve a field, which is understood
366+
by the default `JsonNetSerializer`
345367

346368
[source,csharp]
347369
----
@@ -441,13 +463,15 @@ fieldNameOnB.Should().Be("c.name");
441463

442464
To wrap up, the precedence in which field names are inferred is:
443465

444-
. A hard rename of the property on connection settings using `.Rename()`
466+
. A naming of the property on `ConnectionSettings` using `.Rename()`
445467

446468
. A NEST property mapping
447469

448-
. Ask the serializer if the property has a verbatim value e.g it has an explicit JsonProperty attribute.
470+
. Ask the serializer if the property has a verbatim value, e.g. it has a `JsonPropertyAttribute` when using the default `JsonNetSerializer`
449471

450-
. Pass the MemberInfo's Name to the DefaultFieldNameInferrer, which by default camelCases
472+
. See if the `MemberInfo` has a `DataMemberAttribute` applied
473+
474+
. Pass the `MemberInfo` to the `DefaultFieldNameInferrer`, which by default will camel case the `Name` property
451475

452476
The following example class will demonstrate this precedence
453477

@@ -469,6 +493,9 @@ class Precedence
469493
[JsonProperty("dontaskme")]
470494
public string AskSerializer { get; set; } <4>
471495
496+
[DataMember(Name = "data")]
497+
public string DataMember { get; set; }
498+
472499
public string DefaultFieldNameInferrer { get; set; } <5>
473500
}
474501
----
@@ -489,7 +516,6 @@ Here we create a custom serializer that renames any property named `AskSerialize
489516
class CustomSerializer : JsonNetSerializer
490517
{
491518
public CustomSerializer(IConnectionSettingsValues settings) : base(settings) { }
492-
493519
public override IPropertyMapping CreatePropertyMapping(MemberInfo memberInfo)
494520
{
495521
return memberInfo.Name == nameof(Precedence.AskSerializer)
@@ -516,6 +542,7 @@ usingSettings.Expect("renamed").ForField(Field<Precedence>(p => p.RenamedOnConne
516542
usingSettings.Expect("nestAtt").ForField(Field<Precedence>(p => p.NestAttribute));
517543
usingSettings.Expect("jsonProp").ForField(Field<Precedence>(p => p.JsonProperty));
518544
usingSettings.Expect("ask").ForField(Field<Precedence>(p => p.AskSerializer));
545+
usingSettings.Expect("data").ForField(Field<Precedence>(p => p.DataMember));
519546
usingSettings.Expect("DEFAULTFIELDNAMEINFERRER").ForField(Field<Precedence>(p => p.DefaultFieldNameInferrer));
520547
----
521548

@@ -529,14 +556,16 @@ usingSettings.Expect(new []
529556
"DEFAULTFIELDNAMEINFERRER",
530557
"jsonProp",
531558
"nestAtt",
532-
"renamed"
559+
"renamed",
560+
"data"
533561
}).AsPropertiesOf(new Precedence
534562
{
535563
RenamedOnConnectionSettings = "renamed on connection settings",
536564
NestAttribute = "using a nest attribute",
537565
JsonProperty = "the default serializer resolves json property attributes",
538566
AskSerializer = "serializer fiddled with this one",
539-
DefaultFieldNameInferrer = "shouting much?"
567+
DefaultFieldNameInferrer = "shouting much?",
568+
DataMember = "using a DataMember attribute"
540569
});
541570
542571
public class Parent
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
:ref_current: https://www.elastic.co/guide/en/elasticsearch/reference/5.6
2+
3+
:github: https://github.com/elastic/elasticsearch-net
4+
5+
:nuget: https://www.nuget.org/packages
6+
7+
////
8+
IMPORTANT NOTE
9+
==============
10+
This file has been generated from https://github.com/elastic/elasticsearch-net/tree/5.x/src/Tests/ClientConcepts/HighLevel/Inference/TypeNameInference.doc.cs.
11+
If you wish to submit a PR for any spelling mistakes, typos or grammatical errors for this file,
12+
please modify the original csharp file found at the link and submit the PR with that change. Thanks!
13+
////
14+
15+
[[type-name-inference]]
16+
=== Type name inference
17+
18+
Type names are resolved in NEST by default, by lowercasing the CLR type name
19+
20+
[source,csharp]
21+
----
22+
var settings = new ConnectionSettings();
23+
var resolver = new TypeNameResolver(settings);
24+
var type = resolver.Resolve<Project>();
25+
type.Should().Be("project");
26+
----
27+
28+
[[elasticsearchtype-attribute]]
29+
==== Applying a type name with `ElasticsearchTypeAttribute`
30+
31+
A type name can be applied for a CLR type, using the Name property on `ElasticsearchTypeAttribute`
32+
33+
[source,csharp]
34+
----
35+
[ElasticsearchType(Name = "attributed_project")]
36+
public class AttributedProject { }
37+
38+
var settings = new ConnectionSettings();
39+
var resolver = new TypeNameResolver(settings);
40+
var type = resolver.Resolve<AttributedProject>();
41+
type.Should().Be("attributed_project");
42+
----
43+
44+
[[datacontract-attribute]]
45+
==== Applying a type name with `DataContractAttribute`
46+
47+
Similarly to <<elasticsearchtype-attribute, `ElasticsearchTypeAttribute`>>, a type name can be applied for a
48+
CLR type, using the Name property on `System.Runtime.Serialization.DataContractAttribute`
49+
50+
[source,csharp]
51+
----
52+
[DataContract(Name = "data_contract_project")]
53+
public class DataContractProject { }
54+
55+
var settings = new ConnectionSettings();
56+
var resolver = new TypeNameResolver(settings);
57+
var type = resolver.Resolve<DataContractProject>();
58+
type.Should().Be("data_contract_project");
59+
----
60+
61+
[[type-name-inferrer]]
62+
==== Override type name inferrer
63+
64+
You can provide a delegate to override the default type name inferrer for types
65+
66+
[source,csharp]
67+
----
68+
var settings = new ConnectionSettings()
69+
.DefaultTypeNameInferrer(t=>t.Name.ToLower() + "-suffix");
70+
var resolver = new TypeNameResolver(settings);
71+
var type = resolver.Resolve<Project>();
72+
type.Should().Be("project-suffix");
73+
----
74+

docs/high-level.asciidoc

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ the `.LowLevel` property on `ElasticClient`.
2929

3030
There are a number of conventions that NEST uses for inference of
3131

32-
* <<index-name-inference, Index Names>>
33-
34-
* <<indices-paths, Building a URI path to one or more indices>>
32+
* <<index-name-inference, Index names>>
3533

36-
* <<field-inference, Field names>> and <<property-inference, Property names>>
34+
* <<type-name-inference, Type names>>
3735

3836
* <<ids-inference, Document IDs>>
3937

38+
* <<field-inference, Field names>>
39+
40+
* <<property-inference, Property names>>
41+
4042
* <<document-paths, Building a URI path to a document>>
4143

4244
* <<features-inference, API features>>
@@ -120,10 +122,10 @@ There are several ways to control mapping in NEST
120122

121123
* <<auto-map, Auto mapping (inferred from POCO property types)>>
122124

123-
* <<attribute-mapping, Attribute mapping>>
124-
125125
* <<fluent-mapping, Fluent mapping>>
126126

127+
* <<attribute-mapping, Attribute mapping>>
128+
127129
* <<visitor-pattern-mapping, through the Visitor Pattern>>
128130

129131
and these can be combined to form an overall mapping approach. In addition, there are also ways to control
@@ -134,10 +136,10 @@ and these can be combined to form an overall mapping approach. In addition, ther
134136

135137
include::{output-dir}/mapping/auto-map.asciidoc[]
136138

137-
include::{output-dir}/mapping/attribute-mapping.asciidoc[]
138-
139139
include::{output-dir}/mapping/fluent-mapping.asciidoc[]
140140

141+
include::{output-dir}/mapping/attribute-mapping.asciidoc[]
142+
141143
include::{output-dir}/mapping/visitor-pattern-mapping.asciidoc[]
142144

143145
include::{output-dir}/mapping/ignoring-properties.asciidoc[]
@@ -203,35 +205,37 @@ include::aggregations/reserved-aggregation-names.asciidoc[]
203205

204206
There are a number of conventions that NEST uses for inference of
205207

206-
* <<document-paths, Document paths>>
208+
* <<index-name-inference, Index names>>
207209

208-
* <<features-inference, API features>>
210+
* <<type-name-inference, Type names>>
209211

210-
* <<field-inference, Fields>>
212+
* <<ids-inference, Document Ids>>
211213

212-
* <<ids-inference, Ids>>
214+
* <<field-inference, Document field names>>
213215

214-
* <<index-name-inference, Index names>>
216+
* <<property-inference, Document properties>>
217+
218+
* <<document-paths, Document paths>>
215219

216220
* <<indices-paths, Index paths>>
217221

218-
* <<property-inference, Properties>>
222+
* <<features-inference, API features>>
219223

220-
:includes-from-dirs: client-concepts/high-level/inference
224+
include::{output-dir}/inference/index-name-inference.asciidoc[]
221225

222-
include::client-concepts/high-level/inference/document-paths.asciidoc[]
226+
include::{output-dir}/inference/type-name-inference.asciidoc[]
223227

224-
include::client-concepts/high-level/inference/features-inference.asciidoc[]
228+
include::{output-dir}/inference/ids-inference.asciidoc[]
225229

226-
include::client-concepts/high-level/inference/field-inference.asciidoc[]
230+
include::{output-dir}/inference/field-inference.asciidoc[]
227231

228-
include::client-concepts/high-level/inference/ids-inference.asciidoc[]
232+
include::{output-dir}/inference/property-inference.asciidoc[]
229233

230-
include::client-concepts/high-level/inference/index-name-inference.asciidoc[]
234+
include::{output-dir}/inference/document-paths.asciidoc[]
231235

232-
include::client-concepts/high-level/inference/indices-paths.asciidoc[]
236+
include::{output-dir}/inference/indices-paths.asciidoc[]
233237

234-
include::client-concepts/high-level/inference/property-inference.asciidoc[]
238+
include::{output-dir}/inference/features-inference.asciidoc[]
235239

236240
[[common-types]]
237241
== Common Types
@@ -240,9 +244,9 @@ NEST has a number of types for working with Elasticsearch conventions for
240244

241245
* <<time-units, Time units>>
242246

243-
* <<distance-units, Distance units>>
247+
* <<distance-units, Distance Units>>
244248

245-
* <<date-math-expressions, Dat math expressions>>
249+
* <<date-math-expressions, Date math expressions>>
246250

247251
include::common-options/time-unit/time-units.asciidoc[]
248252

src/Nest/CommonAbstractions/Infer/Field/FieldExpressionVisitor.cs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal class HasVariableExpressionVisitor : ExpressionVisitor
1717
private bool _found;
1818
public bool Found
1919
{
20-
get { return _found; }
20+
get => _found;
2121
// This is only set to true once to prevent clobbering from subsequent node visits
2222
private set { if (!_found) _found = value; }
2323
}
@@ -36,11 +36,10 @@ public override Expression Visit(Expression node)
3636

3737
protected override Expression VisitMethodCall(MethodCallExpression node)
3838
{
39-
if (node.Method.Name == "Suffix" && node.Arguments.Any())
39+
if (node.Method.Name == nameof(SuffixExtensions.Suffix) && node.Arguments.Any())
4040
{
4141
var lastArg = node.Arguments.Last();
42-
var constantExpression = lastArg as ConstantExpression;
43-
this.Found = constantExpression == null;
42+
this.Found = !(lastArg is ConstantExpression);
4443
}
4544
else if (node.Method.Name == "get_Item" && node.Arguments.Any())
4645
{
@@ -53,8 +52,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
5352
if (!isDict)
5453
return base.VisitMethodCall(node);
5554
var lastArg = node.Arguments.Last();
56-
var constantExpression = lastArg as ConstantExpression;
57-
this.Found = constantExpression == null;
55+
this.Found = !(lastArg is ConstantExpression);
5856
}
5957
return base.VisitMethodCall(node);
6058
}
@@ -90,8 +88,7 @@ public string Resolve(MemberInfo info)
9088

9189
var name = info.Name;
9290

93-
IPropertyMapping propertyMapping = null;
94-
if (this._settings.PropertyMappings.TryGetValue(info, out propertyMapping))
91+
if (this._settings.PropertyMappings.TryGetValue(info, out var propertyMapping))
9592
return propertyMapping.Name;
9693

9794
var att = ElasticsearchPropertyAttributeBase.From(info);
@@ -111,7 +108,7 @@ protected override Expression VisitMember(MemberExpression expression)
111108

112109
protected override Expression VisitMethodCall(MethodCallExpression methodCall)
113110
{
114-
if (methodCall.Method.Name == "Suffix" && methodCall.Arguments.Any())
111+
if (methodCall.Method.Name == nameof(SuffixExtensions.Suffix) && methodCall.Arguments.Any())
115112
{
116113
VisitConstantOrVariable(methodCall, _stack);
117114
var callingMember = new ReadOnlyCollection<Expression>(

src/Nest/CommonAbstractions/Infer/TypeName/TypeNameResolver.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Concurrent;
3+
using System.Linq;
4+
using System.Runtime.Serialization;
35

46
namespace Nest
57
{
@@ -21,9 +23,8 @@ public TypeNameResolver(IConnectionSettingsValues connectionSettings)
2123
private string ResolveType(Type type)
2224
{
2325
if (type == null) return null;
24-
string typeName;
2526

26-
if (TypeNames.TryGetValue(type, out typeName))
27+
if (TypeNames.TryGetValue(type, out var typeName))
2728
return typeName;
2829

2930
if (_connectionSettings.DefaultTypeNames.TryGetValue(type, out typeName))
@@ -36,11 +37,16 @@ private string ResolveType(Type type)
3637
if (att != null && !att.Name.IsNullOrEmpty())
3738
typeName = att.Name;
3839
else
39-
typeName = _connectionSettings.DefaultTypeNameInferrer(type);
40+
{
41+
var dataContract = type.GetAttributes<DataContractAttribute>().FirstOrDefault();
42+
typeName = dataContract != null
43+
? dataContract.Name
44+
: _connectionSettings.DefaultTypeNameInferrer(type);
45+
}
46+
if (typeName.IsNullOrEmpty()) throw new ArgumentNullException($"{type.FullName} resolved to an empty string or null");
4047

4148
TypeNames.TryAdd(type, typeName);
4249
return typeName;
4350
}
44-
4551
}
4652
}

0 commit comments

Comments
 (0)