Skip to content

Commit f02ba5e

Browse files
committed
Merge pull request #1072 from elasticsearch/feature/map-attributes-in-code
Allow properties to be ignored in code when serializing/calling MapFromAttributes()
2 parents f3e758f + aa15593 commit f02ba5e

File tree

11 files changed

+301
-51
lines changed

11 files changed

+301
-51
lines changed

src/Nest/Domain/Connection/ConnectionSettings.cs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ string IConnectionSettingsValues.DefaultIndex
9090
private ReadOnlyCollection<Func<Type, JsonConverter>> _contractConverters;
9191
ReadOnlyCollection<Func<Type, JsonConverter>> IConnectionSettingsValues.ContractConverters { get { return _contractConverters; } }
9292

93-
private FluentDictionary<MemberInfo, string> _propertyNames = new FluentDictionary<MemberInfo, string>();
94-
FluentDictionary<MemberInfo, string> IConnectionSettingsValues.PropertyNames { get { return _propertyNames; } }
93+
private FluentDictionary<MemberInfo, PropertyMapping> _propertyMappings = new FluentDictionary<MemberInfo, PropertyMapping>();
94+
FluentDictionary<MemberInfo, PropertyMapping> IConnectionSettingsValues.PropertyMappings { get { return _propertyMappings; } }
9595

9696
public ConnectionSettings(IConnectionPool connectionPool, string defaultIndex)
9797
: base(connectionPool)
@@ -205,12 +205,12 @@ public T MapDefaultTypeNames(Action<FluentDictionary<Type, string>> mappingSelec
205205
return (T)this;
206206
}
207207

208-
public T MapPropertyNamesFor<TDocument>(Action<FluentDictionary<Expression<Func<TDocument, object>>, string>> propertiesSelector)
208+
public T MapPropertiesFor<TDocument>(Action<PropertyMappingDescriptor<TDocument>> propertiesSelector)
209209
{
210210
propertiesSelector.ThrowIfNull("propertiesSelector");
211-
var properties = new FluentDictionary<Expression<Func<TDocument, object>>, string>();
212-
propertiesSelector(properties);
213-
foreach (var p in properties)
211+
var mapper = new PropertyMappingDescriptor<TDocument>();
212+
propertiesSelector(mapper);
213+
foreach (var p in mapper.Mappings)
214214
{
215215
var e = p.Key;
216216
var memberInfoResolver = new MemberInfoResolver(this, e);
@@ -221,14 +221,24 @@ public T MapPropertyNamesFor<TDocument>(Action<FluentDictionary<Expression<Func<
221221
throw new ArgumentException("Expression {0} does contain any member access".F(e));
222222

223223
var memberInfo = memberInfoResolver.Members.Last();
224-
if (_propertyNames.ContainsKey(memberInfo))
224+
if (_propertyMappings.ContainsKey(memberInfo))
225225
{
226-
var mappedAs = _propertyNames[memberInfo];
226+
var newName = p.Value.Name;
227+
var mappedAs = _propertyMappings[memberInfo].Name;
227228
var typeName = typeof (TDocument).Name;
229+
if (mappedAs.IsNullOrEmpty() && newName.IsNullOrEmpty())
230+
throw new ArgumentException("Property mapping '{0}' on type is already ignored"
231+
.F(e, newName, mappedAs, typeName));
232+
if (mappedAs.IsNullOrEmpty())
233+
throw new ArgumentException("Property mapping '{0}' on type {3} can not be mapped to '{1}' it already has an ignore mapping"
234+
.F(e, newName, mappedAs, typeName));
235+
if (newName.IsNullOrEmpty())
236+
throw new ArgumentException("Property mapping '{0}' on type {3} can not be ignored it already has a mapping to '{2}'"
237+
.F(e, newName, mappedAs, typeName));
228238
throw new ArgumentException("Property mapping '{0}' on type {3} can not be mapped to '{1}' already mapped as '{2}'"
229-
.F(e, p.Value, mappedAs, typeName));
239+
.F(e, newName, mappedAs, typeName));
230240
}
231-
_propertyNames.Add(memberInfo, p.Value);
241+
_propertyMappings.Add(memberInfo, p.Value);
232242

233243
}
234244
return (T) this;

src/Nest/Domain/Connection/IConnectionSettingsValues.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public interface IConnectionSettingsValues : IConnectionConfigurationValues
1111
ElasticInferrer Inferrer { get; }
1212
FluentDictionary<Type, string> DefaultIndices { get; }
1313
FluentDictionary<Type, string> DefaultTypeNames { get; }
14-
FluentDictionary<MemberInfo, string> PropertyNames { get; }
14+
FluentDictionary<MemberInfo, PropertyMapping> PropertyMappings { get; }
1515
string DefaultIndex { get; }
1616
Func<string, string> DefaultPropertyNameInferrer { get; }
1717
Func<Type, string> DefaultTypeNameInferrer { get; }

src/Nest/Domain/Mapping/Attributes/IElasticPropertyAttribute.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
namespace Nest {
2-
public interface IElasticPropertyAttribute
1+
namespace Nest
2+
{
3+
public interface IElasticPropertyAttribute : IPropertyMapping
34
{
45
bool AddSortField { get; set; }
56

6-
bool OptOut { get; set; }
7-
string Name { get; set; }
8-
97
FieldType Type { get; }
108

119
TermVectorOption TermVector { get; set; }
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
5+
using System.Reflection.Emit;
6+
using Nest.Resolvers;
7+
8+
namespace Nest
9+
{
10+
11+
public class PropertyMappingDescriptor<TDocument>
12+
{
13+
14+
private readonly IList<KeyValuePair<Expression<Func<TDocument, object>>, PropertyMapping>> _mappings = new List<KeyValuePair<Expression<Func<TDocument, object>>, PropertyMapping>>();
15+
16+
internal IList<KeyValuePair<Expression<Func<TDocument, object>>, PropertyMapping>> Mappings { get { return _mappings; } }
17+
18+
public PropertyMappingDescriptor<TDocument> Rename(Expression<Func<TDocument, object>> property, string propertyName)
19+
{
20+
property.ThrowIfNull("property");
21+
propertyName.ThrowIfNullOrEmpty("propertyName");
22+
this._mappings.Add(new KeyValuePair<Expression<Func<TDocument, object>>, PropertyMapping>(property, propertyName));
23+
return this;
24+
}
25+
26+
public PropertyMappingDescriptor<TDocument> Ignore(Expression<Func<TDocument, object>> property)
27+
{
28+
property.ThrowIfNull("property");
29+
this._mappings.Add(new KeyValuePair<Expression<Func<TDocument, object>>, PropertyMapping>(property, PropertyMapping.Ignored));
30+
return this;
31+
}
32+
}
33+
34+
35+
36+
37+
/// <summary>
38+
/// This class allows you to map aspects of a Type's property
39+
/// that influences how NEST treats it.
40+
/// </summary>
41+
public interface IPropertyMapping
42+
{
43+
/// <summary>
44+
/// Override the json property name of a type
45+
/// </summary>
46+
string Name { get; set; }
47+
/// <summary>
48+
/// Ignore this property completely
49+
/// <pre>- When mapping automatically using MapFromAttributes()</pre>
50+
/// <pre>- When Indexing this type do not serialize whatever this value hold</pre>
51+
/// </summary>
52+
bool OptOut { get; set; } //TODO Rename to Ignore when we get rid of IElasticPropertyAttribute
53+
}
54+
55+
/// <summary>
56+
/// This class allows you to map aspects of a Type's property
57+
/// that influences how NEST treats it.
58+
/// </summary>
59+
public class PropertyMapping : IPropertyMapping
60+
{
61+
public static PropertyMapping Ignored = new PropertyMapping { Ignore = true };
62+
63+
/// <summary>
64+
/// Override the json property name of a type
65+
/// </summary>
66+
public string Name { get; set; }
67+
68+
/// <summary>
69+
/// Ignore this property completely
70+
/// <pre>- When mapping automatically using MapFromAttributes()</pre>
71+
/// <pre>- When Indexing this type do not serialize whatever this value hold</pre>
72+
/// </summary>
73+
public bool Ignore { get; set; }
74+
75+
bool IPropertyMapping.OptOut
76+
{
77+
get { return this.Ignore; }
78+
set { this.Ignore = value; }
79+
}
80+
81+
public static implicit operator PropertyMapping(string propertyName)
82+
{
83+
return propertyName == null ? null : new PropertyMapping() { Name = propertyName };
84+
}
85+
}
86+
}

src/Nest/Nest.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@
148148
<Compile Include="Domain\Cat\CatPluginsRecord.cs" />
149149
<Compile Include="Domain\Cat\CatPendingTasksRecord.cs" />
150150
<Compile Include="Domain\Hit\ExplainGet.cs" />
151+
<Compile Include="Domain\Mapping\PropertyMapping.cs" />
151152
<Compile Include="Domain\Mapping\Descriptors\FieldDataFilterDescriptor.cs" />
152153
<Compile Include="Domain\Mapping\Descriptors\FieldDataFrequencyFilterDescriptor.cs" />
153154
<Compile Include="Domain\Mapping\Descriptors\FieldDataNonStringMappingDescriptor.cs" />

src/Nest/Resolvers/ElasticContractResolver.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,8 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ
170170
property.ShouldSerialize = property.ShouldSerialize == null ? shouldSerialize : (o => property.ShouldSerialize(o) && shouldSerialize(o));
171171
}
172172

173-
var attributes = member.GetCustomAttributes(typeof(IElasticPropertyAttribute), false);
174-
if (attributes == null || !attributes.Any())
175-
return property;
176-
177-
var att = attributes.First() as IElasticPropertyAttribute;
173+
var att = ElasticAttributes.Property(member, this.ConnectionSettings);
174+
if (att == null) return property;
178175
if (!att.Name.IsNullOrEmpty())
179176
property.PropertyName = att.Name;
180177

src/Nest/Resolvers/PropertyNameResolver.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@ public static class ElasticAttributes
1919
private static readonly ConcurrentDictionary<Type, ElasticTypeAttribute> CachedTypeLookups =
2020
new ConcurrentDictionary<Type, ElasticTypeAttribute>();
2121

22-
public static IElasticPropertyAttribute Property(MemberInfo info)
22+
public static IElasticPropertyAttribute Property(MemberInfo info, IConnectionSettingsValues settings = null)
2323
{
24+
if (settings != null)
25+
{
26+
PropertyMapping propertyMapping = null;
27+
if (settings.PropertyMappings.TryGetValue(info, out propertyMapping))
28+
return new ElasticPropertyAttribute {Name = propertyMapping.Name, OptOut = propertyMapping.Ignore};
29+
}
30+
2431
var attributes = info.GetCustomAttributes(typeof(IElasticPropertyAttribute), true);
2532
if (attributes != null && attributes.Any())
2633
return ((IElasticPropertyAttribute)attributes.First());
@@ -61,11 +68,8 @@ public string Resolve(MemberInfo info)
6168
return null;
6269

6370
var name = info.Name;
64-
string resolvedName = null;
65-
if (_settings.PropertyNames.TryGetValue(info, out resolvedName))
66-
return resolvedName;
6771

68-
var att = ElasticAttributes.Property(info);
72+
var att = ElasticAttributes.Property(info, _settings);
6973
if (att != null && !att.Name.IsNullOrEmpty())
7074
return att.Name;
7175

src/Nest/Resolvers/Writers/TypeMappingWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ internal void WriteProperties(JsonWriter jsonWriter)
130130
var properties = this._type.GetProperties();
131131
foreach (var p in properties)
132132
{
133-
var att = ElasticAttributes.Property(p);
133+
var att = ElasticAttributes.Property(p, this._connectionSettings);
134134
if (att != null && att.OptOut)
135135
continue;
136136

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Elasticsearch.Net;
5+
using Elasticsearch.Net.Connection;
6+
using FluentAssertions;
7+
using NUnit.Framework;
8+
using System.Linq.Expressions;
9+
using Newtonsoft.Json;
10+
11+
namespace Nest.Tests.Unit.Internals.Inferno
12+
{
13+
[TestFixture]
14+
public class MapPropertyIgnoreForTests : BaseJsonTests
15+
{
16+
[ElasticType(IdProperty = "Guid")]
17+
internal class SomeClass
18+
{
19+
public MyCustomClass MyCustomClass { get; set; }
20+
[JsonConverter(typeof(DictionaryKeysAreNotPropertyNamesJsonConverter))]
21+
public Dictionary<string, SomeOtherClass> StringDict { get; set; }
22+
public Dictionary<int, MyCustomClass> IntDict { get; set; }
23+
public IList<MyCustomClass> ListOfCustomClasses { get; set; }
24+
public B BInstance { get; set; }
25+
public C CInstance { get; set; }
26+
}
27+
28+
internal class B
29+
{
30+
internal int X { get; set; }
31+
}
32+
33+
internal class C : B
34+
{
35+
}
36+
37+
[ElasticType(IdProperty = "Guid")]
38+
internal class SomeOtherClass
39+
{
40+
[ElasticProperty(Name = "CreateDate")]
41+
public DateTime CreateDate { get; set; }
42+
43+
[ElasticProperty(Name = "custom")]
44+
public MyCustomOtherClass MyCustomOtherClass { get; set; }
45+
}
46+
internal class MyCustomClass
47+
{
48+
[ElasticProperty(Name = "MID")]
49+
public string MyProperty { get; set; }
50+
51+
public override string ToString()
52+
{
53+
return "static id ftw";
54+
}
55+
}
56+
[ElasticType(IdProperty = "Guid", Name = "mycustomother")]
57+
internal class MyCustomOtherClass
58+
{
59+
[ElasticProperty(Name = "MID")]
60+
public string MyProperty { get; set; }
61+
62+
public override string ToString()
63+
{
64+
return "static id ftw";
65+
}
66+
}
67+
68+
internal class UserItemData
69+
{
70+
public string Id { get; set; }
71+
public string UserId { get; set; }
72+
public string Title { get; set; }
73+
public string Hidden { get; set; }
74+
public string UserLabels { get; set; }
75+
}
76+
77+
public MapPropertyIgnoreForTests()
78+
{
79+
80+
}
81+
82+
public ElasticClient ConfigureClient(Action<ConnectionSettings> settingsSelector)
83+
{
84+
var settings = new ConnectionSettings(UnitTestDefaults.Uri, UnitTestDefaults.DefaultIndex);
85+
settingsSelector(settings);
86+
var client = new ElasticClient(settings, connection: new InMemoryConnection());
87+
return client;
88+
}
89+
90+
public IElasticClient ClientWithPropertiesFor<T>(Action<PropertyMappingDescriptor<T>> propertiesSelector)
91+
{
92+
return this.ConfigureClient(c=>c.MapPropertiesFor<T>(propertiesSelector));
93+
}
94+
95+
[Test]
96+
public void Global_Ignore_Should_Be_Adhered()
97+
{
98+
var client = this.ClientWithPropertiesFor<MyCustomClass>(props => props
99+
.Ignore(p=>p.MyProperty)
100+
);
101+
var json = client.Serializer.Serialize(new MyCustomClass
102+
{
103+
MyProperty = "should not be serialized"
104+
});
105+
json.Utf8String().Should().Be("{}");
106+
}
107+
108+
[Test]
109+
public void CanIgnoreTwice_ExceptionMessageMakesSense()
110+
{
111+
var e = Assert.Throws<ArgumentException>(() =>
112+
{
113+
this.ClientWithPropertiesFor<MyCustomClass>(props => props
114+
.Ignore(p => p.MyProperty)
115+
.Ignore(p => p.MyProperty)
116+
);
117+
});
118+
e.Message.Should()
119+
.Contain("is already ignored")
120+
.And.Contain("p => p.MyProperty");
121+
}
122+
123+
[Test]
124+
public void CanNotMapTwiceDifferently_ExceptionMessageMakesSense()
125+
{
126+
var e = Assert.Throws<ArgumentException>(() =>
127+
{
128+
this.ClientWithPropertiesFor<MyCustomClass>(props => props
129+
.Ignore(p => p.MyProperty)
130+
.Rename(p => p.MyProperty, "mahProperty4")
131+
);
132+
});
133+
e.Message.Should()
134+
.Contain("can not be mapped to 'mahProperty4'")
135+
.And.Contain("already has an ignore mapping");
136+
}
137+
138+
[Test]
139+
public void CanNotMapTwiceDifferently_ExceptionMessageMakesSense_AlternativeOrder()
140+
{
141+
var e = Assert.Throws<ArgumentException>(() =>
142+
{
143+
this.ClientWithPropertiesFor<MyCustomClass>(props => props
144+
.Rename(p => p.MyProperty, "mahProperty4")
145+
.Ignore(p => p.MyProperty)
146+
);
147+
});
148+
e.Message.Should()
149+
.Contain("already has a mapping to 'mahProperty4'")
150+
.And.Contain("can not be ignored");
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)