I have some Json response from server, for example:
{"routes" : [
{
"bounds" : {
"northeast" : {
"lat" : 50.4639653,
"lng" : 30.6325177
},
"southwest" : {
"lat" : 50.4599625,
"lng" : 30.6272425
}
},
"copyrights" : "Map data ©2013 Google",
"legs" : [
{
"distance" : {
"text" : "1.7 km",
"value" : 1729
},
"duration" : {
"text" : "4 mins",
"value" : 223
},
And I want to get the value of token 'text' from
"legs" : [
{
"distance" : {
"text" : "1.7 km",
"value" : 1729
},
which is string with value "1.7 km".
Question: is there any build-in function in NewtonsoftJson lib which can be look like:
public string(or JToken) GetJtokenByName(JObject document, string jtokenName)
or do I need to implement some recursive method which will search JToken by name in all JTokens and JArrays in JObject?
If you are looking for a very specific token and know the path to it, you can navigate to it easily using the built-in SelectToken() method. For example:
string distance = jObject.SelectToken("routes[0].legs[0].distance.text").ToString();
If you need to find all occurences of a token with a given name in your JSON, no matter where they occur, then yes you'd need a recursive method. Here is one that might do the trick:
public static class JsonExtensions
{
public static List<JToken> FindTokens(this JToken containerToken, string name)
{
List<JToken> matches = new List<JToken>();
FindTokens(containerToken, name, matches);
return matches;
}
private static void FindTokens(JToken containerToken, string name, List<JToken> matches)
{
if (containerToken.Type == JTokenType.Object)
{
foreach (JProperty child in containerToken.Children<JProperty>())
{
if (child.Name == name)
{
matches.Add(child.Value);
}
FindTokens(child.Value, name, matches);
}
}
else if (containerToken.Type == JTokenType.Array)
{
foreach (JToken child in containerToken.Children())
{
FindTokens(child, name, matches);
}
}
}
}
Here is a demo:
class Program
{
static void Main(string[] args)
{
string json = #"
{
""routes"": [
{
""bounds"": {
""northeast"": {
""lat"": 50.4639653,
""lng"": 30.6325177
},
""southwest"": {
""lat"": 50.4599625,
""lng"": 30.6272425
}
},
""legs"": [
{
""distance"": {
""text"": ""1.7 km"",
""value"": 1729
},
""duration"": {
""text"": ""4 mins"",
""value"": 223
}
},
{
""distance"": {
""text"": ""2.3 km"",
""value"": 2301
},
""duration"": {
""text"": ""5 mins"",
""value"": 305
}
}
]
}
]
}";
JObject jo = JObject.Parse(json);
foreach (JToken token in jo.FindTokens("text"))
{
Console.WriteLine(token.Path + ": " + token.ToString());
}
}
}
Here is the output:
routes[0].legs[0].distance.text: 1.7 km
routes[0].legs[0].duration.text: 4 mins
routes[0].legs[1].distance.text: 2.3 km
routes[0].legs[1].duration.text: 5 mins
This is pretty simple using the json paths and the SelectTokens method on JToken. This method is pretty awesome and supports wilds cards such as the following:
jObject.SelectTokens("routes[*].legs[*].*.text")
Check out this sample code:
private class Program
{
public static void Main(string[] args)
{
string json = GetJson();
JObject jObject = JObject.Parse(json);
foreach (JToken token in jObject.SelectTokens("routes[*].legs[*].*.text"))
{
Console.WriteLine(token.Path + ": " + token);
}
}
private static string GetJson()
{
return #" {
""routes"": [
{
""bounds"": {
""northeast"": {
""lat"": 50.4639653,
""lng"": 30.6325177
},
""southwest"": {
""lat"": 50.4599625,
""lng"": 30.6272425
}
},
""legs"": [
{
""distance"": {
""text"": ""1.7 km"",
""value"": 1729
},
""duration"": {
""text"": ""4 mins"",
""value"": 223
}
},
{
""distance"": {
""text"": ""2.3 km"",
""value"": 2301
},
""duration"": {
""text"": ""5 mins"",
""value"": 305
}
}
]
}]}";
}
}
And here's the output:
routes[0].legs[0].distance.text: 1.7 km
routes[0].legs[0].duration.text: 4 mins
routes[0].legs[1].distance.text: 2.3 km
routes[0].legs[1].duration.text: 5 mins
In case you want all values of a property, regardless of where it occurs, here is an alternative to recursion as described by #brian-rogers, using SelectToken as suggested by #mhand:
To get all values of duration.text, you can use SelectToken and Linq:
var list = jObject.SelectTokens("$..duration.text")
.Select(t => t.Value<string>())
.ToList();
More info: Querying JSON with SelectToken
Related
I now need to deserialize a JSON that looks like this:
{
"arguments": {
"game": [
"--username",
"--version",
"--assetsDir",
{
"rules": [
{
"action": "allow",
"features": {
"is_demo_user": true
}
}
],
"value": "--demo"
},
{
"rules": [
{
"action": "allow",
"features": {
"has_custom_resolution": true
}
}
],
"value": [
"--width",
"--height"
]
}
]
}
}
As you can see, the array named "game" has both "value" and "object" in it. (But the fact is WORSE than this example, the number of elements is NOT certain)
And the data type of arguments.game[*].value is NOT certain, too.
I used to use classes to describe it, but deserialization failed.
Can't seem to describe an array with multiple element types with a class?
I am using Json.NET. Is there any way to deserialize this "game" array.
Thanks.
Is it a requirement to deserialize to an instance of a class? You could use an ExpandoObject:
using System.Dynamic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
Console.WriteLine("Hello, World!");
string json = #"{
""arguments"": {
""game"": [
""--username"",
""--version"",
""--assetsDir"",
{
""rules"": [
{
""action"": ""allow"",
""features"": {
""is_demo_user"": true
}
}
],
""value"": ""--demo""
},
{
""rules"": [
{
""action"": ""allow"",
""features"": {
""has_custom_resolution"": true
}
}
],
""value"": [
""--width"",
""--height""
]
}
]
}
}";
var expConverter = new ExpandoObjectConverter();
dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(json, expConverter);
The obj variable will contain the result of the JSON conversion, then you can traverse the dynamic object in code.
For example, to get a list of strings under 'game':
IList<object> list = new List<object>(obj.arguments.game);
foreach (object str in list)
{
if (str as string != null)
{
Console.WriteLine(str as string);
}
}
I'm having difficulties putting up a code which returns an element in an array of subdocuments. I am actually trying to flatten a document to a new document which is strongly typed. My document is looking like;
{
"_id" : BinData(3, "7FRf4nbe60ev6XmGKBBW4Q=="),
"status" : NumberInt(1),
"title":"Central station",
"attributes" : [
{
"defId" : BinData(3, "QFDtR03NbkqwuhhG76wS8g=="),
"value" : "388",
"name" : null
},
{
"defId" : BinData(3, "RE3MT3clb0OdLEkkqhpFOg=="),
"value" : "",
"name" : null
},
{
"defId" : BinData(3, "pPgJR50h8kGdDaCcH2o17Q=="),
"value" : "Merkez",
"name" : null
}
]}
What I am trying to achieve is;
{
"title":"Central Station",
"value":"388"
}
What I've done already;
using (_dbContext)
{
var filter = Builders<CustomerModel>.Filter.Eq(q => q.Id, Guid.Parse("30B59585-CBFC-4CD5-A43E-0FDB0AE3167A")) &
Builders<CustomerModel>.Filter.ElemMatch(f => f.Attributes, q => q.DefId == Guid.Parse("47ED5040-CD4D-4A6E-B0BA-1846EFAC12F2"));
var projection = Builders<CustomerModel>.Projection.Include(f => f.Title).Include("attributes.value");
var document = _dbContext.Collection<CustomerModel>().Find(filter).Project(projection).FirstOrDefault();
if (document == null)
return null;
return BsonSerializer.Deserialize<TitleAndValueViewModel>(document);
}
Note: TitleAndCodeViewModel contains title and value properties.
This block of code returns;
{{ "_id" : CSUUID("30b59585-cbfc-4cd5-a43e-0fdb0ae3167a"), "title" : "388 güvenevler", "attributes" : [{ "value" : "388" }, { "value" : "" }, { "value" : "Merkez " }] }}
I am trying to get "value":"388" but instead I am getting another two value properties even tough the ElemMatch filter added for subdocument.
Thank you for your help in advance.
Note: I am looking for answers in C# mongodb driver.
Option 1: ( via aggregation)
db.collection.aggregate([
{
$match: {
_id: 5,
"attributes.defId": 1
}
},
{
"$addFields": {
"attributes": {
"$filter": {
"input": "$attributes",
"as": "a",
"cond": {
$eq: [
"$$a.defId",
1
]
}
}
}
}
},
{
$unwind: "$attributes"
},
{
$project: {
_id: 0,
title: 1,
value: "$attributes.value"
}
}
])
Explained:
Match ( good to add index for the matching fields )
Filter only the attribute you need
Unwind to convert the array to object
Project only the necessary output
Playground
Option 2: ( find/$elemMatch )
db.collection.find({
_id: 5,
attributes: {
"$elemMatch": {
"defId": 1
}
}
},
{
_id: 0,
title: 1,
"attributes": {
"$elemMatch": {
"defId": 1
}
}
})
Explained:
Match the element via _id and elemMatch the attribute
Project the necessary elements. ( Note here elemMatch also need to be used to filter the exact match attribute )
( Note this version will not identify if there is second attribute with same attribute.defId , also projection of attribute will be array with single element if found that need to be considered from the app side )
Playground 2
by specifying defId
db.collection.aggregate(
[{
$project: {
title: '$title',
attributes: {
$filter: {
input: '$attributes',
as: 'element',
cond: { $eq: ['$$element.defId', BinData(3, 'QFDtR03NbkqwuhhG76wS8g==')] }
}
}
}
}, {
$project: {
_id: 0,
title: '$title',
value: { $first: '$attributes.value' }
}
}])
result:
{
"title": "Central station",
"value": "388"
}
I have 3 levels of JSON object and trying to sort based on one of the inner-element.
Here is the sample JSON.
public class Program
{
public static void Main()
{
string myJSON = #"
{
""items"": ""2"",
""documents"": [
{
""document"": {
""libraryId"": ""LIB0001"",
""id"": ""100"",
""elements"": {
""heading"": {
""elementType"": ""text"",
""value"": ""My Heading 1""
},
""date"": {
""elementType"": ""datetime"",
""value"": ""2020-07-03T20:30:00-04:00""
}
},
""name"": ""My Name 1 "",
""typeId"": ""10ed9f3f-ab41-45a9-ba24-d988974affa7""
}
},
{
""document"": {
""libraryId"": ""LIB0001"",
""id"": ""101"",
""elements"": {
""heading"": {
""elementType"": ""text"",
""value"": ""My Heading 2""
},
""date"": {
""elementType"": ""datetime"",
""value"": ""2020-07-03T20:30:00-04:00""
}
},
""name"": ""My Name 2"",
""typeId"": ""10ed9f3f-ab41-45a9-ba24-d988974affa7""
}
}
]
}";
JObject resultObject = JObject.Parse(myJSON);
var sortedObj = new JObject(
resultObject.Properties().OrderByDescending(p => p.Value)
);
string output = sortedObj.ToString();
Console.WriteLine(output);
}
}
I would like to sort based the "date" field. Appreciate any help.
You can replace documents json array with sorted one using json path to sort it:
resultObject["documents"] = new JArray(resultObject["documents"]
.Children()
.OrderBy(p => p.SelectToken("$.document.elements.date.value").Value<DateTime>()));
Console.WriteLine(resultObject.ToString());
Or using indexer access:
resultObject["documents"] = new JArray( resultObject["documents"]
.Children()
.OrderBy(p => p["document"]["elements"]["date"]["value"].Value<DateTime>()));
I have this query which works when I run it in Robo 3T:
db.getCollection('groupSchedule').aggregate([{ "$match" : { "GroupId" : ObjectId("598dd346e5549706a80680bf") } },
{ "$lookup" : { "from" : "schedule", "localField" : "ScheduleIds", "foreignField" : "_id", "as" : "Schedule" } },
{ "$unwind" : "$Schedule" },
{ "$match" : {"$or": [{ "Schedule.End.ByDate" : {"$gte":new Date()}},{ "Schedule.End.ByDate" : null}] } },
{ "$group" : { "_id" : "$GroupId", "SurveyIds" : { "$addToSet" : "$Schedule.SurveyId" }, "ScheduleIds" : { "$addToSet" : "$Schedule._id" } } },
{ "$project" : { "_id" : 0, "SurveyIds" : 1, "ScheduleIds": 1 } }])
However, when I try to do the same thing using the C# driver as it blows up saying that:
"Duplicate element name 'Schedule.End.ByDate'.",
Here's the code:
return new List<BsonDocument>
{
Common.Util.MongoUtils.Match(new BsonDocument { { "GroupId", groupId } }),
Common.Util.MongoUtils.Lookup(scheduleCollections, "ScheduleIds", "_id", "Schedule"),
Common.Util.MongoUtils.Unwind("$Schedule"),
Common.Util.MongoUtils.Match(new BsonDocument
{
{
"$or", new BsonDocument
{
{
"Schedule.End.ByDate", BsonNull.Value
},
{
"Schedule.End.ByDate", new BsonDocument
{
{
"$gte", DateTime.UtcNow
}
}
}
}
}
}),
Group(),
Common.Util.MongoUtils.Project(new BsonDocument
{
{ "_id", 0 },
{ "SurveyIds", 1 },
{ "Schedules", 1 }
})
};
Any thoughts?
By using BsonDocument for your $or operator, you're effectively trying to create the following:
"$or": {
"Schedule.End.ByDate": null,
"Schedule.End.ByDate": { "$gte" : ISODate("...") }
}
If we look again at your error message:
"Duplicate element name 'Schedule.End.ByDate'.",
It's clear that you have duplicated the Schedule.End.ByDate element name, which is invalid and not what was intended.
Instead, you want to use BsonArray to wrap two separate objects, in order to produce the result you have in your Robo 3T query. To achieve that, you can use the following adaptation of your C# code:
"$or", new BsonArray
{
new BsonDocument
{
{ "Schedule.End.ByDate", BsonNull.Value }
},
new BsonDocument
{
{
"Schedule.End.ByDate", new BsonDocument
{
{ "$gte", DateTime.UtcNow }
}
}
}
}
This produces the following, which matches your Robo 3T query for the $or section:
{ "$or" : [
{ "Schedule.End.ByDate" : null },
{ "Schedule.End.ByDate" : { "$gte" : ISODate("...") } }
] }
I would like to create an Aggregation on my data to get the total amount of counts for specific tags for a collection of books in my .Net application.
I have the following Book class.
public class Book
{
public string Id { get; set; }
public string Name { get; set; }
[BsonDictionaryOptions(DictionaryRepresentation.Document)]
public Dictionary<string, string> Tags { get; set; }
}
And when the data is saved, it is stored in the following format in MongoDB.
{
"_id" : ObjectId("574325a36fdc967af03766dc"),
"Name" : "My First Book",
"Tags" : {
"Edition" : "First",
"Type" : "HardBack",
"Published" : "2017",
}
}
I've been using facets directly in MongoDB and I am able to get the results that I need by using the following query:
db.{myCollection}.aggregate(
[
{
$match: {
"Name" : "SearchValue"
}
},
{
$facet: {
"categorizedByTags" : [
{
$project :
{
Tags: { $objectToArray: "$Tags" }
}
},
{ $unwind : "$Tags"},
{ $sortByCount : "$Tags"}
]
}
},
]
);
However I am unable to transfer this over to the .NET C# Driver for Mongo. How can I do this using the .NET C# driver?
Edit - I will ultimately be looking to query the DB on other properties of the books as part of a faceted book listings page, such as Publisher, Author, Page count etc... hence the usage of $facet, unless there is a better way of doing this?
I would personally not use $facet here since you've only got one pipeline which kind of defeats the purpose of $facet in the first place...
The following is simpler and scales better ($facet will create one single potentially massive document).
db.collection.aggregate([
{
$match: {
"Name" : "My First Book"
}
}, {
$project: {
"Tags": {
$objectToArray: "$Tags"
}
}
}, {
$unwind: "$Tags"
}, {
$sortByCount: "$Tags"
}, {
$group: { // not really needed unless you need to have all results in one single document
"_id": null,
"categorizedByTags": {
$push: "$$ROOT"
}
}
}, {
$project: { // not really needed, either: remove _id field
"_id": 0
}
}])
This could be written using the C# driver as follows:
var collection = new MongoClient().GetDatabase("test").GetCollection<Book>("test");
var pipeline = collection.Aggregate()
.Match(b => b.Name == "My First Book")
.Project("{Tags: { $objectToArray: \"$Tags\" }}")
.Unwind("Tags")
.SortByCount<BsonDocument>("$Tags");
var output = pipeline.ToList().ToJson(new JsonWriterSettings {Indent = true});
Console.WriteLine(output);
Here's the version using a facet:
var collection = new MongoClient().GetDatabase("test").GetCollection<Book>("test");
var project = PipelineStageDefinitionBuilder.Project<Book, BsonDocument>("{Tags: { $objectToArray: \"$Tags\" }}");
var unwind = PipelineStageDefinitionBuilder.Unwind<BsonDocument, BsonDocument>("Tags");
var sortByCount = PipelineStageDefinitionBuilder.SortByCount<BsonDocument, BsonDocument>("$Tags");
var pipeline = PipelineDefinition<Book, AggregateSortByCountResult<BsonDocument>>.Create(new IPipelineStageDefinition[] { project, unwind, sortByCount });
// string based alternative version
//var pipeline = PipelineDefinition<Book, BsonDocument>.Create(
// "{ $project :{ Tags: { $objectToArray: \"$Tags\" } } }",
// "{ $unwind : \"$Tags\" }",
// "{ $sortByCount : \"$Tags\" }");
var facetPipeline = AggregateFacet.Create("categorizedByTags", pipeline);
var aggregation = collection.Aggregate().Match(b => b.Name == "My First Book").Facet(facetPipeline);
var output = aggregation.Single().Facets.ToJson(new JsonWriterSettings { Indent = true });
Console.WriteLine(output);