Use-case:
PHP symfony project which has to communicate with a C# back-end with Mongo. In the PHP front-end it is possible to make a query to get data from Mongo. This query is send via an API (XML). The C# back-end deserializes this XML to get the objects. Then I want to execute an Linq-to-objects query (which is the query send via the API) on a collection in my memory. So I wanted to make my own "LinqBuilder" so I can query the objects and return them to my PHP front-end.
I have the following object:
public class MongoDoc
{
public int Id { get; set; }
public string Kind { get; set; }
public BsonDocument Data { get; set; }
}
Below is an example of what I'm trying to achieve.
var list = source.Where(x => x.Data["Identifier"] == "H7PXXK").ToList(); // source is collection of MongoDoc objects
The code line above is what I want to build with expressions because it has to be dynamic. What I did achieve is to query the "Kind" property of my MongoDoc object as follow:
ParameterExpression _expr = Expression.Parameter(typeof(MongoDoc), "x");
expression = Expression.Equal(
Expression.PropertyOrField(_expr, "Kind"),
Expression.Constant("KindValue")
);
This will produce the following lamdba:
x => (x.Kind == "KindValue")
That is correct, but now I need to get the property Identifier in the BsonDocument property Data. Normally it would be something like above: x => x.Data["Identifier"] == "Value". This is exactly what my problem is. How can I achieve this?
think it should be something like that.
var _expr = Expression.Parameter(typeof(MongoDoc), "x");
//x.Data
Expression member = Expression.PropertyOrField(_expr, "Data");
//x.Data["Identifier"]
member = Expression.Property(member, "Item", new Expression[]{Expression.Constant("Identifier")});
//x.Data["Identifier"] == "H7PXXK"
member = Expression.Equal(member, Expression.Constant((BsonValue)"H7PXXK"));
EDIT :
from your comment, it should be
//x.Data["MoreData"]
member = Expression.Property(member, "Item", new Expression[]{Expression.Constant("MoreData")});
//x.Data["MoreData"]["Identifier"]
member = Expression.Property(member, "Item", new Expression[]{Expression.Constant("Identifier")});
Related
Scenario:
I have to export an excel file which will contain list of Parts. We have enabled the user to select the columns and get only selected columns' data in the exported file. Since this is a dynamic report, I am not using any concrete class to map the report as this will result in exporting empty column headers in the report, which is unnecessary. I am using Dynamic Linq to deal with this scenario.
I have a list of dynamic objects fetched from dynamic linq.
[
{"CleanPartNo":"Test","Description":"test","AliasPartNo":["258","145","2313","12322"]},
{"CleanPartNo":"Test1","Description":"test1","AliasPartNo":[]}
]
How can I get 4 rows out of this json like
Please note that I cannot use a strongly typed object to deserialize/ Map it using JSON.Net
Update
Following is the code:
public class Part
{
public int Id { get; set; }
public string CleanPartNo { get; set; }
public string Description { get; set; }
public List<PartAlias> AliasPartNo { get; set; }
}
public class PartAlias
{
public int PartId { get; set; }
public int PartAliasId { get; set; }
public string AliasPartNo { get; set; }
}
var aliases = new List<PartAlias> {
new PartAlias{AliasPartNo="258" },
new PartAlias{AliasPartNo="145" },
new PartAlias{AliasPartNo="2313" },
new PartAlias{AliasPartNo="12322" }
};
List<Part> results = new List<Part> {
new Part{CleanPartNo="Test", Description= "test", PartAlias=aliases },
new Part{CleanPartNo="Test1", Description= "test1" }
};
var filters = "CleanPartNo,Description, PartAlias.Select(AliasPartNo) as AliasPartNo";
var dynamicObject = JsonConvert.SerializeObject(results.AsQueryable().Select($"new ({filters})"));
in the dynamicObject variable I get the json mentioned above
Disclaimer: The following relies on anonymous classes, which is not exactly the same as dynamic LINQ (not at all), but I figured that it may help anyway, depending on your needs, hence I decided to post it.
To flatten your list, you could go with a nested Select, followed by a SelectMany (Disclaimer: This assumes that every part has at least one alias, see below for the full code)
var flattenedResult = result.Select(part => part.AliasPartNumber.Select(alias => new
{
CleanPartNo = part.CleanPartNo,
Description = part.Description,
AliasPartNo = alias.AliasPartNo
})
.SelectMany(part => part);
You are first projecting your items from result (outer Select). The projection projects each item to an IEnumerable of an anonymous type in which each item corresponds to an alias part number. Since the outer Select will yield an IEnumerable<IEnumerable> (or omething alike), we are using SelectMany to get a single IEnumerable of all the items from your nested IEnumerables. You can now serialize this IEnumerable of instances of an anonymous class with JsonConvert
var json = sonConvert.SerializeObject(flatResults);
Handling parts without aliases
If there are no aliases, the inner select will yield an empty IEnumerable, hence we will have to introduce a special case
var selector = (Part part) => part.AliasPartNumber?.Any() == true
? part.AliasPartNumber.Select(alias => new
{
CleanPartNo = part.CleanPartNo,
Description = part.Description,
AliasPartNo = alias.AliasPartNo
})
: new[]
{
new
{
CleanPartNo = part.CleanPartNo,
Description = part.Description,
AliasPartNo = alias.AliasPartNo
}
};
var flattenedResult = result.Select(selector).SelectMany(item => item);
From json you provided you can get values grouped by their name in this way:
var array = JArray.Parse(json);
var lookup = array.SelectMany(x => x.Children<JProperty>()).ToLookup(x => x.Name, x => x.Value);
then this is just a manner of simple loop over the lookup to fill the excel columns.
However, I would suggest to do the flatenning before JSON. I tried for some time to make it happen even without knowing the names of the columns that are arrays, but I failed, and since it's your job, I won't try anymore :P
I think the best way here would be to implement custom converter that would just multiply objects for properties that are arrays. If you do it well, you would get infinite levels completely for free.
This is my MongoDB document structure:
{
string _id;
ObservableCollection<DataElement> PartData;
ObservableCollection<DataElement> SensorData;
...
other ObservableCollection<DataElement> fields
...
other types and fields
...
}
Is there any possibility to retrieve a concatenation of fields with the type ObservableCollection<DataElement>? Using LINQ I would do something like
var query = dbCollection
.AsQueryable()
.Select(x => new {
data = x
.OfType(typeof(ObservableCollection<DataElement>))
.SelectMany(x => x)
.ToList()
});
or alternatively
data = x.Where(y => typeof(y) == typeof(ObservableCollection<DataElement>)
.SelectMany(x => x).ToList()
Unfortunately .Where() and .OfType() do not work on documents, only on queryables/lists, so is there another possibility to achieve this? The document structure must stay the same.
Edit:
After dnickless answer I tried it with method 1b), which works pretty well for getting the fields thy way they are in the collection. Thank you!
Unfortunately it wasn't precisely what I was looking for, as I wanted to be all those fields with that specific type put together in one List, at it would be returned by the OfType or Where(typeof) statement.
e.g. data = [x.PartData , x.SensorData, ...] with data being an ObsverableCollection<DataElement>[], so that I can use SelectMany() on that to finally get the concatenation of all sequences.
Sorry for asking the question unprecisely and not including the last step of doing a SelectMany()/Concat()
Finally I found a solution doing this, but it doesn't seem very elegant to me, as it needs one concat() for every element (and I have more of them) and it needs to make a new collection when finding a non-existing field:
query.Select(x => new
{
part = x.PartData ?? new ObservableCollection<DataElement>(),
sensor = x.SensorData ?? new ObservableCollection<DataElement>(),
}
)
.Select(x => new
{
dataElements = x.part.Concat(x.sensor)
}
).ToList()
In order to limit the fields returned you would need to use the MongoDB Projection feature in one way or the other.
There's a few alternatives depending on your specific requirements that I can think of:
Option 1a (fairly static approach): Create a custom type with only the fields that you are interested in if you know them upfront. Something like this:
public class OnlyWhatWeAreInterestedIn
{
public ObservableCollection<DataElement> PartData { get; set; }
public ObservableCollection<DataElement> SensorData { get; set; }
// ...
}
Then you can query your Collection like that:
var collection = new MongoClient().GetDatabase("test").GetCollection<OnlyWhatWeAreInterestedIn>("test");
var result = collection.Find(FilterDefinition<OnlyWhatWeAreInterestedIn>.Empty);
Using this approach you get a nicely typed result back without the need for custom projections.
Option 1b (still pretty static): A minor variation of Option 1a, just without a new explicit type but a projection stage instead to limit the returned fields. Kind of like that:
var collection = new MongoClient().GetDatabase("test").GetCollection<Test>("test");
var result = collection.Find(FilterDefinition<Test>.Empty).Project(t => new { t.PartData, t.SensorData }).ToList();
Again, you get a nicely typed C# entity back that you can continue to operate on.
Option 2: Use some dark reflection magic in order to dynamically create a projection stage. Downside: You won't get a typed instance reflecting your properties but instead a BsonDocument so you will have to deal with that afterwards. Also, if you have any custom MongoDB mappings in place, you would need to add some code to deal with them.
Here's the full example code:
First, your entities:
public class Test
{
string _id;
public ObservableCollection<DataElement> PartData { get; set; }
public ObservableCollection<DataElement> SensorData { get; set; }
// just to have one additional property that will not be part of the returned document
public string TestString { get; set; }
}
public class DataElement
{
}
And then the test program:
public class Program
{
static void Main(string[] args)
{
var collection = new MongoClient().GetDatabase("test").GetCollection<Test>("test");
// insert test record
collection.InsertOne(
new Test
{
PartData = new ObservableCollection<DataElement>(
new ObservableCollection<DataElement>
{
new DataElement(),
new DataElement()
}),
SensorData = new ObservableCollection<DataElement>(
new ObservableCollection<DataElement>
{
new DataElement(),
new DataElement()
}),
TestString = "SomeString"
});
// here, we use reflection to find the relevant properties
var allPropertiesThatWeAreLookingFor = typeof(Test).GetProperties().Where(p => typeof(ObservableCollection<DataElement>).IsAssignableFrom(p.PropertyType));
// create a string of all properties that we are interested in with a ":1" appended so MongoDB will return these fields only
// in our example, this will look like
// "PartData:1,SensorData:1"
var mongoDbProjection = string.Join(",", allPropertiesThatWeAreLookingFor.Select(p => $"{p.Name}:1"));
// we do not want MongoDB to return the _id field because it's not of the selected type but would be returned by default otherwise
mongoDbProjection += ",_id:0";
var result = collection.Find(FilterDefinition<Test>.Empty).Project($"{{{mongoDbProjection}}}").ToList();
Console.ReadLine();
}
}
I'm new to MongoDB so this might be a naive question, yet I have not found any relevant/up to date information by googling around: I am trying to use the MongoDB C# driver (version 2.2.4) to compose a LINQ-based query, one piece at a time, from a received filter POCO object, like this:
IQueryable<BsonDocument> parts = collection.AsQueryable();
if (filter.Name != null)
parts = parts.Where(d => d["Name"].Equals(filter.Name));
// ... etc; I'll then call ToList() to get results ...
Now, one of my filter properties is a string array, meaning that I should match any document whose field Vendor (a string property in the MongoDB document) is equal to any of the strings in the array (like MongoDB native $in: https://docs.mongodb.com/manual/reference/operator/query/in/).
To this end, I tried with Contains (the special case for a 1-sized array is just an optimization):
if (filter.Vendors != null && filter.Vendors.Length > 0)
{
parts = filter.Vendors.Length == 1
? parts.Where(d => d["Vendor"].Equals(filter.Vendors[0]))
: parts.Where(d => filter.Vendors.Contains(d["Vendor"].AsString));
}
This compiles, but throws an ArgumentException: "Expression of type 'MongoDB.Bson.BsonValue' cannot be used for parameter of type 'System.String' of method 'Boolean Contains[String](System.Collections.Generic.IEnumerable`1[System.String], System.String)'".
Looking at http://mongodb.github.io/mongo-csharp-driver/2.2/reference/driver/crud/linq/, there is nothing about Contains or $in; yet, from https://jira.mongodb.org/browse/CSHARP-462 it seems that the driver should now be capable of handling that method.
BTW, the same happens if I slightly change the code to:
parts.Where(d => filter.Vendors.Any(s=> d["Vendor"].Equals(s)));
which does not involve Contains at all. The exception message complains about not being able to use BsonValue for string, yet that BsonValue is right a string. Could anyone suggest a solution?
The exception messages dance around the idea of fully embracing BsonValue to let mongo handle the types instead of trying to cast to string. I got it to work having Vendors as type List<BsonValue>.
class Filter
{
public List<BsonValue> Vendors { get; set; }
}
...
var list = parts.Where(d => filter.Vendors.Contains(d["Vendor"]));
foreach (var document in list)
{
Console.WriteLine(document["Name"]);
}
Another alternative is to map your documents to a C# class instead of using BsonDocument as the collection type.
class MyDocument
{
public ObjectId Id { get; set; }
public string Name { get; set; }
public string Vendor { get; set; }
}
...
var collection = db.GetCollection <MyDocument> ("parts");
For unit testing purposes, I'd like to test my class mappings without reading and writing documents into the MongoDB database. To handle special cases such as circular parent / child references and read only properties, I've used BsoncClassMap.RegisterClassMap< MyType>(...) with some custom mappings overriding the default AutoMap(); generated mappings.
Does anyone know how to convert a BsonDocument into the desired strongly typed object without making a round trip to the database? The driver is doing this when going to and from the data store. My goal would be to use the same logic that the MongoDB C# driver is using internally to test the serialization to / from a C# domain object into a BsonDocument.
I'm able to use the Bson extension method ToBsonDocument() to convert a C# object into a BsonDocument? The piece that I'm lacking is the reverse of the process - essentially a BsonDocument.ToObject< MyType>();.
Is this possible with the latest version of the official MongoDB C# driver? It seems like it should be - I'm wondering if I'm just blind and am missing the obvious.
The MongoDB Driver does provide a method for deserializing from Bson to your type. The BsonSerializer can be found in MongoDB.Bson.dll, in the MongoDB.Bson.Serialization namespace.
You can use the BsonSerializer.Deserialize<T>() method. Some example code would be
var obj = new MyClass { MyVersion = new Version(1,0,0,0) };
var bsonObject = obj.ToBsonDocument();
var myObj = BsonSerializer.Deserialize<MyClass>(bsonObject);
Console.WriteLine(myObj);
Where MyClass is defined as
public class MyClass
{
public Version MyVersion {get; set;}
}
I hope this helps.
A straight forward way if you want to map rows fetched from mongoDB to a class within your code is as below
//Connect and Query from MongoDB
var db = client.GetDatabase("blog");
var col = db.GetCollection<BsonDocument>("users");
var result = await col.Find(new BsonDocument("Email",model.Email)).ToListAsync();
//read first row from the result
var user1 = result[0];
result[0] would be say "{ "_id" : ObjectId("569c05da09f251fb0ee33f5f"), "Name" : "fKfKWCc", "Email" : "pujkvBFU#kQKeYnabk.com" }"
//A user class with name and email
User user = new User();
// assign
User.Name = user1[1].ToString(); // user1[1] is "fKfKWCc"
User.Email = user1[2].ToString(); // user1[2] is "pujkvBFU#kQKeYnabk.com"
In case of you need a part of object, for example:
You have entity Teacher:
public class Teacher
{
public string Mail {get; set;}
public IEnumerable<Course> Courses {get; set;}
public string Name {get; set;}
}
And entity Course:
public class Course
{
public int CurseCode {get; set;}
public string CourseName {get; set;}
}
And you need only "Courses" from "Teacher" entity, you can use:
var db = conection.GetDatabase("school");
var collection = db.GetCollection<Teacher>("teachers"); // Or your collection Name
string mailForSearch="teacher#school.com"; // param for search in linq
var allCoursesBson = collection.Find(x => x.Mail == mailForSearch).Project(Builders<Teacher>.Projection.Include(x => x.Courses).Exclude("_Id")).ToList();
// allCoursesBson is BsonDocument list, then use a first BsonDocument an convert to string for convert to IEnumerable<Courses> type with BsonSerializer.Deserialize
string allCoursesText = resp.FirstOrDefault()[0].ToString();
IEnumerable<Courses> allCourses = BsonSerializer.Deserialize<IEnumerable<Courses>>(allCoursesText);
Now, you have a courses list from taecher and convert BsonDocument answer into "IEnumerable".
Use yield keyword to return data as you want.
public IEnumerable<string> GetMongoFields(string collectionName)
{
var connectionString = ConfigurationManager.ConnectionStrings[DbConfig.GetMongoDb()].ConnectionString;
var databaseName = MongoUrl.Create(connectionString).DatabaseName;
MongoClient client = new MongoClient(connectionString);
var server = client.GetServer();
var db = server.GetDatabase(databaseName);
var collection = db.GetCollection<BsonDocument>(collectionName);
var list = collection.FindAll().ToList();
yield return list.ToJson();
}
I'm trying to convert a List<Topic> to an anonymous or dynamic type via linq projection... I'm am using the following code, but it doesn't seem to work properly. It returns the dynamic type without error, however, if I try to enumerate the children field (list<object/topic>) then it says
Results View = The type '<>f__AnonymousType6<id,title,children>' exists in both 'MyWebCore.dll' and 'MvcExtensions.dll'
Strange.
Here is the code I am using:
protected dynamic FlattenTopics()
{
Func<List<Topic>, object> _Flatten = null; // satisfy recursion re-use
_Flatten = (topList) =>
{
if (topList == null) return null;
var projection = from tops in topList
select new
{
id = tops.Id,
title = tops.Name,
children = _Flatten(childs.Children.ToList<Topic>())
};
dynamic transformed = projection;
return transformed;
};
var topics = from tops in Repository.Query<Topic>().ToList()
select new
{
id = tops.Id,
title = tops.Name,
children = _Flatten(tops.Children.ToList<Topic>())
};
return topics;
}
All i'm doing is flattening a list of self containing objects - basically it's a list of POCOs that will be stuffed into a tree view (jstree).
The Topic class is defined as:
public class Topic
{
public Guid Id {get;set;}
public string Name {get;set;}
public List<Topic> Children {get;set;}
}
And here is an example of what the first member of the returned dynamic object looks like:
[0] = {
id = {566697be-b336-42bc-9549-9feb0022f348},
title = "AUTO",
children = {System.Linq.Enumerable.SelectManyIterator
<MyWeb.Models.Topic,
MyWeb.Models.Topic,
<>f__AnonymousType6<System.Guid,string,object>
>}
}
Why do you have the same LINQ code twice? After you define your _Flatten func, you can just call it immediately - var topics = _Flatten(Repository.Query<Topic>().ToList().
It looks like you're creating two identical anonymous types, one inside the _Flatten func and one outside it. I would think the compiler is smart enough to handle that, but try changing your call to explicitly use _Flatten, see if it solves the problem.
Here is the proper way - have to load into the a DTO / POCO and return that:
_Flatten = (topList) =>
{
if (topList == null) return null;
var projection = from tops in topList
//from childs in tops.Children
select new JsTreeJsonNode
{
//id = tops.Id.ToString(),
data = tops.Name,
attr = setAttributes(tops.Id.ToString(), tops.URI),
state = "closed",
children = _Flatten(tops.Children)
};
return projection.ToList();
};