Skip to content

Commit 97455b8

Browse files
authored
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
1 parent 6cc3e50 commit 97455b8

File tree

11 files changed

+256
-64
lines changed

11 files changed

+256
-64
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
@@ -114,7 +114,8 @@ Expect("name")
114114
To ease creating a `Field` instance from expressions, there is a static `Infer` class you can use
115115

116116
[TIP]
117-
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>()`
117+
This example uses the https://msdn.microsoft.com/en-us/library/sf0df423.aspx#Anchor_0[static import] `using static Nest.Infer;`
118+
in the using directives to shorthand `Nest.Infer.Field<T>()`
118119
to simply `Field<T>()`. Be sure to include this static import if copying any of these examples.
119120

120121
[source,csharp]
@@ -322,6 +323,7 @@ Expect("leadDeveloper.firstName.raw.evendeeper").WhenSerializing(multiSuffixFiel
322323
Expect("metadata.hardcoded.raw.evendeeper").WhenSerializing(multiSuffixFieldExpressions[4]);
323324
----
324325

326+
[[field-name-attribute]]
325327
==== Attribute based naming
326328

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

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

344366
[source,csharp]
345367
----
@@ -439,10 +461,11 @@ fieldNameOnB.Should().Be("c.name");
439461

440462
To wrap up, the precedence in which field names are inferred is:
441463

442-
1) A hard rename of the property on connection settings using `.PropertyName()`
443-
2) A NEST PropertyNameAttribute
444-
3) Ask the serializer if the property has a verbatim value, e.g it has a JsonPropertyAttribute.
445-
4) Pass the MemberInfo's Name to the DefaultFieldNameInferrer, which by default will camelCase
464+
1) A naming of the property on `ConnectionSettings` using `.PropertyName()`
465+
2) A NEST `PropertyNameAttribute`
466+
3) Ask the serializer if the property has a verbatim value, e.g. it has a `JsonPropertyAttribute` if using {nuget}/NEST.JsonNetSerializer[`JsonNetSerializer`]
467+
4) See if the `MemberInfo` has a `DataMemberAttribute` applied
468+
5) Pass the `MemberInfo` to the `DefaultFieldNameInferrer`, which by default will camel case the `Name` property
446469

447470
The following example class will demonstrate this precedence
448471

@@ -467,6 +490,9 @@ class Precedence
467490
[PropertyName("dontaskme"),JsonProperty("dontaskme")]
468491
public string AskSerializer { get; set; } <5>
469492
493+
[DataMember(Name = "data")]
494+
public string DataMember { get; set; }
495+
470496
public string DefaultFieldNameInferrer { get; set; } <6>
471497
}
472498
----
@@ -515,6 +541,7 @@ usingSettings.Expect("nestAtt").ForField(Field<Precedence>(p => p.NestAttribute)
515541
usingSettings.Expect("nestProp").ForField(Field<Precedence>(p => p.NestProperty));
516542
usingSettings.Expect("jsonProp").ForField(Field<Precedence>(p => p.JsonProperty));
517543
usingSettings.Expect("ask").ForField(Field<Precedence>(p => p.AskSerializer));
544+
usingSettings.Expect("data").ForField(Field<Precedence>(p => p.DataMember));
518545
usingSettings.Expect("DEFAULTFIELDNAMEINFERRER").ForField(Field<Precedence>(p => p.DefaultFieldNameInferrer));
519546
----
520547

@@ -529,15 +556,17 @@ usingSettings.Expect(new []
529556
"jsonProp",
530557
"nestProp",
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
NestProperty = "using a nest property",
538566
JsonProperty = "the default serializer resolves json property attributes",
539567
AskSerializer = "serializer fiddled with this one",
540-
DefaultFieldNameInferrer = "shouting much?"
568+
DefaultFieldNameInferrer = "shouting much?",
569+
DataMember = "using a DataMember attribute"
541570
});
542571
543572
public class Parent

docs/client-concepts/high-level/inference/types-and-relations-inference.asciidoc

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ please modify the original csharp file found at the link and submit the PR with
1515
[[types-and-relations-inference]]
1616
=== Type and Relation names inference
1717

18-
Type names are resolved in NEST by lowercasing the CLR type name
18+
Type names are resolved in NEST by default, by lowercasing the CLR type name
1919

2020
[source,csharp]
2121
----
@@ -25,6 +25,39 @@ var type = resolver.Resolve<Project>();
2525
type.Should().Be("project");
2626
----
2727

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+
2861
[[default-type-name]]
2962
==== Default type name
3063

@@ -58,7 +91,8 @@ type.Should().Be("project-suffix");
5891
----
5992

6093
[[relation-names]]
61-
==== Relation names
94+
[float]
95+
=== Relation names
6296

6397
Prior to Elasticsearch 6.x you could have multiple types per index. They acted as a discrimatory column but were often
6498
confused with tables. The fact that the mapping API's treated them as seperate entities did not help.

docs/high-level.asciidoc

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -209,37 +209,37 @@ include::aggregations/reserved-aggregation-names.asciidoc[]
209209

210210
There are a number of conventions that NEST uses for inference of
211211

212-
* <<document-paths, Document paths>>
212+
* <<index-name-inference, Index names>>
213213

214-
* <<field-inference, Fields>>
214+
* <<types-and-relations-inference, Type and Relation names>>
215215

216-
* <<ids-inference, Ids>>
216+
* <<ids-inference, Document Ids>>
217217

218-
* <<index-name-inference, Index names>>
218+
* <<field-inference, Document field names>>
219219

220-
* <<indices-paths, Index paths>>
220+
* <<property-inference, Document properties>>
221221

222-
* <<property-inference, Properties>>
222+
* <<document-paths, Document paths>>
223223

224-
* <<routing-inference, Routing>>
224+
* <<indices-paths, Index paths>>
225225

226-
:includes-from-dirs: client-concepts/high-level/inference
226+
* <<routing-inference, Routing>>
227227

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

230-
include::client-concepts/high-level/inference/field-inference.asciidoc[]
230+
include::{output-dir}/inference/types-and-relations-inference.asciidoc[]
231231

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

234-
include::client-concepts/high-level/inference/index-name-inference.asciidoc[]
234+
include::{output-dir}/inference/field-inference.asciidoc[]
235235

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

238-
include::client-concepts/high-level/inference/property-inference.asciidoc[]
238+
include::{output-dir}/inference/document-paths.asciidoc[]
239239

240-
include::client-concepts/high-level/inference/routing-inference.asciidoc[]
240+
include::{output-dir}/inference/indices-paths.asciidoc[]
241241

242-
include::client-concepts/high-level/inference/types-and-relations-inference.asciidoc[]
242+
include::{output-dir}/inference/routing-inference.asciidoc[]
243243

244244
[[common-types]]
245245
== Common Types

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: 9 additions & 2 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
{
@@ -35,8 +37,13 @@ private string ResolveType(Type type)
3537
typeName = att.Name;
3638
else
3739
{
38-
var inferredType =_connectionSettings.DefaultTypeNameInferrer(type);
39-
typeName = !inferredType.IsNullOrEmpty() ? inferredType : _connectionSettings.DefaultTypeName;
40+
var dataContract = type.GetAttributes<DataContractAttribute>().FirstOrDefault();
41+
if (dataContract != null) typeName = dataContract.Name;
42+
else
43+
{
44+
var inferredType =_connectionSettings.DefaultTypeNameInferrer(type);
45+
typeName = !inferredType.IsNullOrEmpty() ? inferredType : _connectionSettings.DefaultTypeName;
46+
}
4047
}
4148
if (typeName.IsNullOrEmpty()) throw new ArgumentNullException($"{type.FullName} resolved to an empty string or null");
4249

src/Nest/CommonAbstractions/SerializationBehavior/PropertyMapping.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Concurrent;
22
using System.Reflection;
3+
using System.Runtime.Serialization;
34
using Newtonsoft.Json;
45

56
namespace Nest
@@ -59,10 +60,11 @@ public virtual IPropertyMapping CreatePropertyMapping(MemberInfo memberInfo)
5960
private static IPropertyMapping PropertyMappingFromAttributes(MemberInfo memberInfo)
6061
{
6162
var jsonProperty = memberInfo.GetCustomAttribute<JsonPropertyAttribute>(true);
62-
var rename = memberInfo.GetCustomAttribute<PropertyNameAttribute>(true);
63+
var dataMemberProperty = memberInfo.GetCustomAttribute<DataMemberAttribute>(true);
64+
var propertyName = memberInfo.GetCustomAttribute<PropertyNameAttribute>(true);
6365
var ignore = memberInfo.GetCustomAttribute<IgnoreAttribute>(true);
64-
if (jsonProperty == null && ignore == null && rename == null) return null;
65-
return new PropertyMapping {Name = rename?.Name ?? jsonProperty?.PropertyName, Ignore = ignore != null};
66+
if (jsonProperty == null && ignore == null && propertyName == null && dataMemberProperty == null) return null;
67+
return new PropertyMapping {Name = propertyName?.Name ?? jsonProperty?.PropertyName ?? dataMemberProperty?.Name, Ignore = ignore != null};
6668
}
6769
}
6870

0 commit comments

Comments
 (0)