Retreiveing Relationship in Neoj4Client gives Deserialization exception - asp.net

I'm trying to retrieve a relationship with an attribute from my DB. I'm using Neo4jClient and .NET .
This is what I've tried so far :
var client = new GraphClient(new Uri("http://localhost:11007"), "neo4j", "jobsjobs");
var JsonContractResolver = new CamelCasePropertyNamesContractResolver();
client.ConnectAsync().Wait();
var createQuery = client.Cypher
.Match("matched=(a:Technology{name:{to},title:{level}})<-[rel:HOP]-(b:Technology{name:{from},title:{level}})")
.WithParam("to", "ArangoDB")
.WithParam("level", "Junior")
.WithParam("from", "Big Table")
.Return(rel => rel.As<RelationshipInstance<Hop>>()).ResultsAsync;
foreach (var item in createQuery.Result)
{
Console.WriteLine(item.Data.Cost);
}
This is my 'Hop' relationship class:
public class Hop : Relationship,
IRelationshipAllowingSourceNode<Technology>,
IRelationshipAllowingTargetNode<Technology>
{
[JsonProperty("cost")]
public long? Cost { get; set; }
public Hop() : base(-1, null) { }
public Hop(NodeReference targetNode) : base(targetNode)
{
}
public const string TypeKey = "HOP";
public override string RelationshipTypeKey
{
get { return TypeKey; }
}
This is the error that I've been getting for the past 6 hours:
"One or more errors occurred. (Neo4j returned a valid response, however Neo4jClient was unable to deserialize into the object structure you supplied.\r\n\r\nFirst, try and review the exception below to work out what broke.\r\n\r\nIf it's not obvious, you can ask for help at http://stackoverflow.com/questions/tagged/neo4jclient\r\n\r\nInclude the full text of this exception, including this message, the stack trace, and all of the inner exception details.\r\n\r\nInclude the full type definition of <>f__AnonymousType11[[System.String, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].\r\n\r\nInclude this raw JSON, with any sensitive values replaced with non-sensitive equivalents:\r\n\r\n (Parameter 'content')) " at Neo4jClient.Serialization.CypherJsonDeserializer1.Deserialize(String content, Boolean isHttp)\r\n at Neo4jClient.GraphClient.Neo4jClient.IRawGraphClient.ExecuteGetCypherResultsAsync[TResult](CypherQuery query)" "
EDIT : Adding the JSON format of the HOP relationship :
{
"identity": 1515,
"start": 1495,
"end": 1491,
"type": "HOP",
"properties": {
"cost": 3
}
}
This is the JSON format of the nodes:
{
"identity": 1491,
"labels": [
"Technology"
],
"properties": {
"name": "ArangoDB",
"title": "Junior"
}
}

With the relationships, to get the properties, you just need to supply a standard POCO, so instead of your Hop class, you should go for:
public class Hop
{
[JsonProperty("cost")]
public long? Cost {get;set;}
}
which changes your query to:
var createQueryTask = client
.Cypher
.Match("matched=(a:Technology{name:{to},title:{level}})<-[rel:HOP]-(b:Technology{name:{from},title:{level}})")
.WithParams(new { to = "ArangoDB", level = "Junior", from = "Big Table"})
.Return(rel => rel.As<Hop>()).ResultsAsync;

Related

Newtonsoft.json: cut JSON according to json path whitelist

Suppose, I have some complex JSON:
{
"path1": {
"path1Inner1": {
"id": "id1"
},
"path1Inner2": {
"id": "id2"
}
},
"path2": {
"path2Inner1": {
"id": "id3"
},
"path2Inner2": {
"id": "id4",
"key": "key4"
}
}
}
And there is also some whitelist of json path expressions, for example:
$.path1.path1Inner1
$.path2.path2Inner2.key
I want to leave in the JSON tree only nodes and properties that match the "whitelist", so, the result would be:
{
"path1": {
"path1Inner1": {
"id": "id1"
}
},
"path2": {
"path2Inner2": {
"key": "key4"
}
}
}
I.e. this is not just a selection by JSON path (which is a trivial task) but the nodes and properties have to keep the initial place in the source JSON tree.
First of all, I have many thanks for this and this answers. They became the starting point of my analysis of the problem.
Those answers present two different approaches in achieving the goal of "whitelisting" by paths. The first one rebuilds the whitelist paths structure from scratch (i.e. starting from the empty object creates the needed routes). The implementation parses the string paths and tries to rebuild the tree based on the parsed path. This approach needs very handy work of considering all possible types of paths and therefore might be error-prone. You can find some of the mistakes I have found in my comment to the answer.
The second approach is based on the json.net object tree API (Parent, Ancestors, Descendants, etc. etc.). The algorithm traverses the tree and removes paths that are not "whitelisted". I find that approach much easier and much less error-prone as well as supporting the wide range of cases "in one go".
The algorithm I have implemented is in many points similar to the second answer but, I think, is much easier in implementation and understanding. Also, I don't think it is worse in its performance.
public static class JsonExtensions
{
public static TJToken RemoveAllExcept<TJToken>(this TJToken token, IEnumerable<string> paths) where TJToken : JContainer
{
HashSet<JToken> nodesToRemove = new(ReferenceEqualityComparer.Instance);
HashSet<JToken> nodesToKeep = new(ReferenceEqualityComparer.Instance);
foreach (var whitelistedToken in paths.SelectMany(token.SelectTokens))
TraverseTokenPath(whitelistedToken, nodesToRemove, nodesToKeep);
//In that case neither path from paths has returned any token
if (nodesToKeep.Count == 0)
{
token.RemoveAll();
return token;
}
nodesToRemove.ExceptWith(nodesToKeep);
foreach (var notWhitelistedNode in nodesToRemove)
notWhitelistedNode.Remove();
return token;
}
private static void TraverseTokenPath(JToken value, ISet<JToken> nodesToRemove, ISet<JToken> nodesToKeep)
{
JToken? immediateValue = value;
do
{
nodesToKeep.Add(immediateValue);
if (immediateValue.Parent is JObject or JArray)
{
foreach (var child in immediateValue.Parent.Children())
if (!ReferenceEqualityComparer.Instance.Equals(child, value))
nodesToRemove.Add(child);
}
immediateValue = immediateValue.Parent;
} while (immediateValue != null);
}
}
To compare the JToken instances it's necessary to use reference equality comparer since some of JToken types use "by value" comparison like JValue does. Otherwise, you could get buggy behaviour in some cases.
For example, having source JSON
{
"path2":{
"path2Inner2":[
"id",
"id"
]
}
}
and a path $..path2Inner2[0] you will get the result JSON
{
"path2":{
"path2Inner2":[
"id",
"id"
]
}
}
instead of
{
"path2":{
"path2Inner2":[
"id"
]
}
}
As far as .net 5.0 is concerned the standard ReferenceEqualityComparer can be used. If you use an earlier version of .net you might need to implement it.
Let's suppose that you have a valid json inside a sample.json file:
{
"path1": {
"path1Inner1": {
"id": "id1"
},
"path1Inner2": {
"id": "id2"
}
},
"path2": {
"path2Inner1": {
"id": "id3"
},
"path2Inner2": {
"id": "id4",
"key": "key4"
}
}
}
Then you can achieve the desired output with the following program:
static void Main()
{
var whitelist = new[] { "$.path1.path1Inner1", "$.path2.path2Inner2.key" };
var rawJson = File.ReadAllText("sample.json");
var semiParsed = JObject.Parse(rawJson);
var root = new JObject();
foreach (var path in whitelist)
{
var value = semiParsed.SelectToken(path);
if (value == null) continue; //no node exists under the path
var toplevelNode = CreateNode(path, value);
root.Merge(toplevelNode);
}
Console.WriteLine(root);
}
We read the json file and semi parse it to a JObject
We define a root where will merge the processing results
We iterate through the whitelisted json paths to process them
We retrieve the actual value of the node (specified by the path) via the SelectToken call
If the path is pointing to a non-existing node then SelectToken returns null
Then we create a new JObject which contains the full hierarchy and the retrieved value
Finally we merge that object to the root
Now let's see the two helper methods
static JObject CreateNode(string path, JToken value)
{
var entryLevels = path.Split('.').Skip(1).Reverse().ToArray();
return CreateHierarchy(new Queue<string>(entryLevels), value);
}
We split the path by dots and remove the first element ($)
We reverse the order to be able to put it into a Queue
We want to build up the hierarchy from inside out
Finally we call a recursive function with the queue and the retrieved value
static JObject CreateHierarchy(Queue<string> pathLevels, JToken currentNode)
{
if (pathLevels.Count == 0) return currentNode as JObject;
var newNode = new JObject(new JProperty(pathLevels.Dequeue(), currentNode));
return CreateHierarchy(pathLevels, newNode);
}
We first define the exit condition to make sure that we will not create an infinite recursion
We create a new JObject where we specify the name and value
The output of the program will be the following:
{
"path1": {
"path1Inner1": {
"id": "id1"
}
},
"path2": {
"path2Inner2": {
"key": "key4"
}
}
}

Odata [EnableQuery] attribute makes EF Core crash on high requests amount

In my Odata controller I Use [EnableQuery(EnsureStableOrdering = false)]. Stable ordering is because of some custom Order by which I need in my code. Example in the mentioned Issue above
The problem however is with the [EnableQuery] attribute in general. When I have this attribute in the controller on high load, which means more than 150 request in a minute at the same endpoint I get a lot of the following exception:
"System.InvalidOperationException","Message":"A second operation was
started on this context before a previous operation completed. This is
usually caused by different threads concurrently using the same
instance of DbContext."
I have used testing tool which send that kind of request to my endpoind (Result is around 240 requests in a minute with 1s delay between each) and most of the requests succeed, like maybe around 85% - 90% of them, however the failed requests get the exception above.
I needed some time to figure out what is happening, but when I remove the [EnableQuery] attribute everything was fine. No exception on any level of requests amount.
Here some code:
public class OfficeDtoController : ODataController
{
private readonly IOfficesService _officesService;
public OfficeDtoController(IOfficesService officesService)
{
_officesService = officesService;
}
[HttpGet]
[EnableQuery(EnsureStableOrdering = false)]
public IActionResult GetOfficeDto(ODataQueryOptions<OfficeDto> queryOptions, Guid? id = null, string sortColumn = "", string sortOrder = "", int? top = null, int? skip = null)
{
IQueryable<OfficeDto> officeDto = _officesService.GetOffices(id, sortColumn, sortOrder, top, skip);
return Ok(officeDto);
}
}
public IQueryable<OfficeDto> GetOffices(Guid? id, string sortColumn, string sortOrder, int? top, int? skip)
{
IQueryable<Office> query = dbSet;
query = query
.Where(z => !z.Deleted);
var list = query.Select(x =>
new OfficeDto()
{
Id = x.Id,
ZipCode = x.ZipCode,
Active = x..Active,
Employees = x.XrefOfficesEmployees
.OrderBy(y => y.Order)
.Select(y => new IdAndName()
{
Value = y.EmployeeId,
Text = y.Employee.FirstName + " " + y.Employee.LastName,
Order = y.Order
})
});
switch (sortColumn)
{
case "employees":
if (sortOrder == "desc")
{
officeDto = officeDto.OrderByDescending(b => b.Employees.Select(a => a.Text).FirstOrDefault( ));
}
else
{
officeDto = officeDto.OrderBy(b => b.Employees.Select(a => a.Text).FirstOrDefault( ));
}
}
return list;
}
Here is the example URL:
api/OfficeDto?$count=true&$top=20&$skip=0&$select=id,zipcode,employees&$filter=(active
eq true)
Maybe there is problem in the communication between EF Core and Odata.
I use ASP.NET Wen API on NET 5 and the following NuGets:
Microsoft.AspNetCore.OData 7.5.4
Microsoft.Data.OData 5.8.4
Microsoft.OData.Core 7.8.1
Microsoft.EntityFrameworkCore 5.0.2
EDIT:
For ones who asked about DI:
services.AddDbContext<MyContext>( );
services.AddTransient<IOffcesService, OfficesService>( );
However I have tried to initialize everything in the Controller (DB Context and BusinessService) because at first my suggestion was about DI, however it didnt help.

StackOverflowException running SerializeObject

I want to replicate the TypeNameHandling = TypeNameHandling.Objects setting but have my own property name, rather than $type, and have it find the objects based on the simple class name rather than have the assembly referenced.
I have a nested object model that I am trying to sterilise using the Newtonsoft tool. When I run it I get a System.StackOverflowException and I really cant figure out why... I have reviewed Custom JsonConverter WriteJson Does Not Alter Serialization of Sub-properties and the solution there does not work natively within Newtonsoft and thus ignores all of the Newtonsoft native attributes.
If I pass a single convertor (all the object inherit from IOptions) I get only the top-level object with the required ObjectType:
{
"ObjectType": "ProcessorOptionsA",
"ReplayRevisions": true,
"PrefixProjectToNodes": false,
"CollapseRevisions": false,
"WorkItemCreateRetryLimit": 5,
"Enabled": true,
"Endpoints": null,
"ProcessorEnrichers": [
{
"Enabled": true
},
{
"Enabled": true
}
]
}
I have 4 classes that all have my custom OptionsJsonConvertor set as the convertor.
[JsonConverter(typeof(OptionsJsonConvertor<IProcessorEnricherOptions>))]
public interface IProcessorEnricherOptions : IEnricherOptions
{
}
[JsonConverter(typeof(OptionsJsonConvertor<IProcessorOptions>))]
public interface IProcessorOptions : IProcessorConfig, IOptions
{
List<IEndpointOptions> Endpoints { get; set; }
List<IProcessorEnricherOptions> ProcessorEnrichers { get; set; }
IProcessorOptions GetDefault();
}
[JsonConverter(typeof(OptionsJsonConvertor<IEndpointOptions>))]
public interface IEndpointOptions : IOptions
{
[JsonConverter(typeof(StringEnumConverter))]
public EndpointDirection Direction { get; set; }
public List<IEndpointEnricherOptions> EndpointEnrichers { get; set; }
}
[JsonConverter(typeof(OptionsJsonConvertor<IEndpointEnricherOptions>))]
public interface IEndpointEnricherOptions : IEnricherOptions
{
}
The object model does not nest the same object type at any point, but does have List<IEndpointEnricherOptions> contained within List<IEndpointOptions> contained within List<IProcessorOptions>.
"Processors": [
{
"ObjectType": "ProcessorOptionsA",
"Enabled": true,
"ProcessorEnrichers": [
{
"ObjectType": "ProcessorEnricherOptionsA",
"Enabled": true
},
{
"ObjectType": "ProcessorEnricherOptionsB",
"Enabled": true,
}
],
"Endpoints": [
{
"ObjectType": "EndpointOptionsA",
"EndpointEnrichers": [
{
"ObjectType": "EndpointEnricherOptionsA",
"Enabled": true,
}
]
},
{
"ObjectType": "EndpointOptionsA",
"EndpointEnrichers": [
{
"ObjectType": "EndpointEnricherOptionsA",
"Enabled": true,
},
{
"ObjectType": "EndpointEnricherOptionsB",
"Enabled": true,
}
]
}
]
}
]
I want to replicate the TypeNameHandling = TypeNameHandling.Objects setting but have my own property name as well as finding the objects, but everything else should be the same.
Right now I have public class OptionsJsonConvertor<TOptions> : JsonConverter which works for a single nested list, but no sub-lists.
public override void WriteJson(JsonWriter writer,object value,JsonSerializer serializer)
{
JToken jt = JToken.FromObject(value);
if (jt.Type != JTokenType.Object)
{
jt.WriteTo(writer);
}
else
{
JObject o = (JObject)jt;
o.AddFirst(new JProperty("ObjectType", value.GetType().Name));
o.WriteTo(writer);
}
}
If I remove all of the [JsonConverter] class attributes then it executes and adds ObjectType to the IProcessorOptions, but not to any of the subtypes. However, with those attributes, I get System.StackOverflowException on JToken jt = JToken.FromObject(value);
I had thought that this was due to it being the same object type, however even with 4 custom JsonConverter classes that don't share a common codebase I get the same exception.
I'm stumped and really don't want to have the ugly "$type" = "MyAssmbly.Namespace.Class, Assembly" node!
UPDATE Even if I only have the OptionsJsonConvertor<IProcessorOptions> enabled on the Class IProcessorOptions I get a System.StackOverflowException.
OK, so the resolution for this was somewhat of a compromise. We now have no customer JsonConverter types in the system and instead, use ISerializationBinder.
public class OptionsSerializationBinder : ISerializationBinder
{
public void BindToName(Type serializedType, out string assemblyName, out string typeName)
{
assemblyName = null;
typeName = serializedType.Name;
}
public Type BindToType(string assemblyName, string typeName)
{
Type type = AppDomain.CurrentDomain.GetAssemblies()
.Where(a => !a.IsDynamic)
.SelectMany(a => a.GetTypes())
.FirstOrDefault(t => t.Name.Equals(typeName) || t.FullName.Equals(typeName));
if (type is null || type.IsAbstract || type.IsInterface)
{
Log.Warning("Unable to load Processor: {typename}", typeName);
throw new InvalidOperationException();
}
return type;
}
}
Setting the Assembly option to null is critical to maintain the friendly names that we want.
private static JsonSerializerSettings GetSerializerSettings(TypeNameHandling typeHandling = TypeNameHandling.Auto)
{
return new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = typeHandling,
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
SerializationBinder = new OptionsSerializationBinder(),
Formatting = Formatting.Indented
};
}
We can then set SerializationBinder and the default the TypeNameHandling to auto. We found when setting this to Object that it was too greedy and tried to write a $type for generic lists and such, creating a nasty look that did not serilise. Auto provided the right level for us.
If you need to use Object which will also apply to the root of your object map then you may need to create a customer wrapper class around any List<> or Dictionary<> object that you want to serialise to make sure it gets a friendly name.

How convert IConfigurationRoot or IConfigurationSection to JObject/JSON

I have the following code in my Program.cs:
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("clientsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"clientsettings.{host.GetSetting("environment")}.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
I want to convert the result of building my configuration to JObject\Json for sending to the client. How can I do it?
and I don't want to create my custom class for my settings.
My answer: merge
public static JObject GetSettingsObject(string environmentName)
{
object[] fileNames = { "settings.json", $"settings.{environmentName}.json" };
var jObjects = new List<object>();
foreach (var fileName in fileNames)
{
var fPath = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + fileName;
if (!File.Exists(fPath))
continue;
using (var file = new StreamReader(fPath, Encoding.UTF8))
jObjects.Add(JsonConvert.DeserializeObject(file.ReadToEnd()));
}
if (jObjects.Count == 0)
throw new InvalidOperationException();
var result = (JObject)jObjects[0];
for (var i = 1; i < jObjects.Count; i++)
result.Merge(jObjects[i], new JsonMergeSettings
{
MergeArrayHandling = MergeArrayHandling.Merge
});
return result;
}
Since configuration is actually just a key value store where the keys have a certain format to represent a path, serializing it back into a JSON is not that simple.
What you could do is recursively traverse through the configuration children and write its values to a JObject. This would look like this:
public JToken Serialize(IConfiguration config)
{
JObject obj = new JObject();
foreach (var child in config.GetChildren())
{
obj.Add(child.Key, Serialize(child));
}
if (!obj.HasValues && config is IConfigurationSection section)
return new JValue(section.Value);
return obj;
}
Note that this is extremely limited in how the output looks. For example, numbers or booleans, which are valid types in JSON, will be represented as strings. And since arrays are represented through numerical key paths (e.g. key:0 and key:1), you will get property names that are strings of indexes.
Let’s take for example the following JSON:
{
"foo": "bar",
"bar": {
"a": "string",
"b": 123,
"c": true
},
"baz": [
{ "x": 1, "y": 2 },
{ "x": 3, "y": 4 }
]
}
This will be represented in configuration through the following key paths:
"foo" -> "bar"
"bar:a" -> "string"
"bar:b" -> "123"
"bar:c" -> "true"
"baz:0:x" -> "1"
"baz:0:y" -> "2"
"baz:1:x" -> "3"
"baz:1:y" -> "4"
As such, the resulting JSON for the above Serialize method would look like this:
{
"foo": "bar",
"bar": {
"a": "string",
"b": "123",
"c": "true"
},
"baz": {
"0": { "x": "1", "y": "2" },
"1": { "x": "3", "y": "4" }
}
}
So this will not allow you to get back the original representation. That being said, when reading the resulting JSON again with Microsoft.Extensions.Configuration.Json, then it will result in the same configuration object. So you can use this to store the configuration as JSON.
If you want anything prettier than that, you will have to add logic to detect array and non-string types, since both of these are not concepts of the configuration framework.
I want to merge appsettings.json and appsettings.{host.GetSetting("environment")}.json to one object [and send that to the client]
Keep in mind that environment-specific configuration files often contain secrets that shouldn’t leave the machine. This is also especially true for environment variables. If you want to transmit the configuration values, then make sure not to include the environment variables when building the configuration.
The configuration data is represented by a flattened collection of KeyValuePair<string, string>. You could create a dictionary from it and serialize that to JSON. However, that will probably not give you the desired result:
Configuration.AsEnumerable().ToDictionary(k => k.Key, v => v.Value);
Also, please take in mind that this configuration object will contain environment variables, you definitely don't want to send these to the client.
A better option might be to first bind the configuration to your POCO's and serialize those to JSON:
var appConfig = new AppConfig();
Configuration.Bind(appConfig);
var json = JsonConvert.SerializeObject(appConfig);
public class AppConfig
{
// Your settings here
public string Foo { get; set; }
public int Bar { get; set; }
}
The resultant IConfiguration object from the Build() method will encompass all of your configuration sources, and will merge based on the priority order defined by the order in which you added your config sources.
In your case this would be:
clientsettings.json
clientsettings.env.json
Environment Variables
You wont need to worry about merging sources manually or loading the files, as its already done for you.
To improve on poke's answer, I came up with this:
private JToken Serialize(IConfiguration config)
{
JObject obj = new JObject();
foreach (var child in config.GetChildren())
{
if (child.Path.EndsWith(":0"))
{
var arr = new JArray();
foreach (var arrayChild in config.GetChildren())
{
arr.Add(Serialize(arrayChild));
}
return arr;
}
else
{
obj.Add(child.Key, Serialize(child));
}
}
if (!obj.HasValues && config is IConfigurationSection section)
{
if (bool.TryParse(section.Value, out bool boolean))
{
return new JValue(boolean);
}
else if (decimal.TryParse(section.Value, out decimal real))
{
return new JValue(real);
}
else if (long.TryParse(section.Value, out int integer))
{
return new JValue(integer);
}
return new JValue(section.Value);
}
return obj;
}
The code above accounts for data types such as boolean, long & decimal.
long & decimal are the largest data types available for integers so will encompass any smaller values like short or float.
The code will also construct your arrays properly, so you end up with a like for like representation of all of your config in one json file.
Here is Tom's solution converted to use System.Text.Json.
static internal JsonNode? Serialize(IConfiguration config)
{
JsonObject obj = new();
foreach (var child in config.GetChildren())
{
if (child.Path.EndsWith(":0"))
{
var arr = new JsonArray();
foreach (var arrayChild in config.GetChildren())
{
arr.Add(Serialize(arrayChild));
}
return arr;
}
else
{
obj.Add(child.Key, Serialize(child));
}
}
if (obj.Count() == 0 && config is IConfigurationSection section)
{
if (bool.TryParse(section.Value, out bool boolean))
{
return JsonValue.Create(boolean);
}
else if (decimal.TryParse(section.Value, out decimal real))
{
return JsonValue.Create(real);
}
else if (long.TryParse(section.Value, out long integer))
{
return JsonValue.Create(integer);
}
return JsonValue.Create(section.Value);
}
return obj;
}
// Use like this...
var json = Serialize(Config);
File.WriteAllText("out.json",
json.ToJsonString(new JsonSerializerOptions() { WriteIndented = true}));
Do you really want to sent to client all your environment variables (.AddEnvironmentVariables()), connections string and all other stuff in appsettings??? I recommend you do not do this.
Instead, make one class (say ClientConfigOptions), configure it binding using services.Configure<ClientConfigOptions>(configuration.GetSection("clientConfig")) and send it to client.
With this approach, you may also tune your ClientConfigOptions with Actions, copy some values from different appsetting paths, etc.

Having trouble with Nested-Objects using Nest

When I try to index a doc of my defined type, having a list which is supposed to be mapped as a nested-object ("type":"nested"), it's getting mapped as a regular object type.
Take a look at the code:
I've got a simple class like this one:
[ElasticType()]
public class MyJob
{
[ValueFieldAttribute]
public int jobCode { get; set; }
[ValueFieldAttribute(Type = FieldType.nested)]
public IList<JobProfessionalFieldInfo> JobProfessionalFields { get; set; }
}
The code for the JobProfessionalFieldInfo class is:
[ElasticType()]
public class JobProfessionalFieldInfo
{
[ValueFieldAttribute]
public int JobId { get; set; }
[ValueFieldAttribute]
public int CategoryId { get; set; }
}
The code for the ValueFieldAttribute class is:
public class ValueFieldAttribute : ElasticPropertyAttribute
{
public ValueFieldAttribute()
: base()
{
this.Store = false;
this.Index = FieldIndexOption.not_analyzed;
}
}
My program:
static void Main(string[] args)
{
ConnectionSettings node = new ConnectionSettings(new Uri("http://localhost:9200"));
node.SetDefaultIndex("jobs");
ElasticClient client = new ElasticClient(node);
List<JobProfessionalFieldInfo> list = new List<JobProfessionalFieldInfo>();
list.Add(new JobProfessionalFieldInfo { CategoryId = 1, JobId = 1 });
list.Add(new JobProfessionalFieldInfo { CategoryId = 2, JobId = 2 });
var res = client.Index<MyJob>(new MyJob
{
jobCode = 1,
JobProfessionalFields = list
},"jobs", "MyJob",1);
}
Now, when I run it, it indexes the object successfully... BUT(!!) when I get the mapping of the index with GET jobs/MyJob/_mapping, I see that jobProfessionalFields has no "type":"nested" in its mapping.
That results in a query like the following one, returning the indexed doc while it's not supposed to get it back (that's what nested-type is for right?..):
GET jobs/_search
{
"query":
{
"bool":
{
"must":
[
{"match": {"jobId":1}},
{"match": {"categoryId":2}}
]
}
}
}
It's not the end:
I'd a look at here,
there the guy that answered tells that when we use annotations we need to manually call the createIndex and Map methods, but the problem is that I don't have any generic Map method...!
Take a look at here: (just to make you get into the link - here's its start..)
namespace Nest
{
public partial class ElasticClient...
And I don't know how to use the non-generic Map method to put the mapping of my MyJob class.
How can I cause this stuff to map the jobProfessionalFields as nested-type dudes?
Thanks for any help of you guys!
OK, got it LOL!
The MapFromAttributes<> is the right generic method for putting the mapping (at least in the current Nest version I'm using - 0.12.0).
But it demands a manual call for the index creationg, o.w it gives an IndexMissing exception (like the guy in the above mentioned link said).
client.CreateIndex("jobs", new IndexSettings { });
var res = client.MapFromAttributes<MyJob>("jobs","MyJob");
But that's really interesting why isn't it enough to just define the
[ElasticProperty(Type = FieldType.nested)],
in order to get the nested mapping though..
I would be glad to get an answer for that one.

Resources