MongoDb c# driver find item in array by field value - c#

i found the way to check is the value contains in simple array :
var filter = Builders<Post>.Filter.AnyEq(x => x.Tags, "mongodb");
But how to find a complex item with many fields by a concrete field ?
I found the way to write it via the dot notation approach with BsonDocument builder, but how can i do it with typed lambda notations ?
upd
i think it some kind of
builderInst.AnyIn(p => p.ComplexCollection.Select(ml => ml.Id), mlIds)
but can't check right now, is anyone could help ?

There is ElemMatch
var filter = Builders<Post>.Filter.ElemMatch(x => x.Tags, x => x.Name == "test");
var res = await collection.Find(filter).ToListAsync()

Here's an example that returns a single complex item from an array (using MongoDB.Driver v2.5.0):
Simple Data Model
public class Zoo
{
public List<Animal> Animals { get; set; }
}
public class Animal
{
public string Name { get; set; }
}
Option 1 (Aggregation)
public Animal FindAnimalInZoo(string animalName)
{
var zooWithAnimalFilter = Builders<Zoo>.Filter
.ElemMatch(z => z.Animals, a => a.Name == animalName);
return _db.GetCollection<Zoo>("zoos").Aggregate()
.Match(zooWithAnimalFilter)
.Project<Animal>(
Builders<Zoo>.Projection.Expression<Animal>(z =>
z.Animals.FirstOrDefault(a => a.Name == animalName)))
.FirstOrDefault(); // or .ToList() to return multiple
}
Option 2 (Filter & Linq) This was about 5x slower for me
public Animal FindAnimalInZoo(string animalName)
{
// Same as above
var zooWithAnimalFilter = Builders<Zoo>.Filter
.ElemMatch(z => z.Animals, a => a.Name == animalName);
var zooWithAnimal = _db.GetCollection<Zoo>("zoos")
.Find(zooWithAnimalFilter)
.FirstOrDefault();
return zooWithAnimal.Animals.FirstOrDefault(a => a.Name == animalName);
}

You need the $elemMatch operator. You could use Builders<T>.Filter.ElemMatch or an Any expression:
Find(x => x.Tags.Any(t => t.Name == "test")).ToListAsync()
http://mongodb.github.io/mongo-csharp-driver/2.0/reference/driver/expressions/#elemmatch

As of the 2.4.2 release of the C# drivers, the IFindFluent interface can be used for querying on array element. ElemMatch cannot be used on an array of strings directly, whereas the find interface will work on either simple or complex types (e.g. 'Tags.Name') and is strongly typed.
FilterDefinitionBuilder<Post> tcBuilder = Builders<Post>.Filter;
FilterDefinition<Post> tcFilter = tcBuilder.Eq("Tags","mongodb") & tcBuilder.Eq("Tags","asp.net");
...
await myCollection.FindAsync(tcFilter);
Linq driver uses the aggregation framework, but for a query with no aggregation operators a find is faster.
Note that this has been broken in previous versions of the driver so the answer was not available at the time of original posting.

Related

How to write LINQ query to sql IN

I want to query my cosmosDB to get List of Documents of type ZonesDO && whose id are in UserPreferance.Zones My UserPreferance class is :
public class UserPreference
{
[JsonProperty("zones")]
public List<Zone> Zones { get; set; }
}
and Zone Class is:
public class Zone
{
[JsonProperty("id")]
public override Guid Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("category")]
public string Category { get; set; }
}
I am trying this query but not able to complete it.
var zones = DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri)
.Where(z => z.Type == typeof(ZoneDO).ToString() &&
*z.Id in user.UserPreference.Zones.ids*)// here I need the solution
.AsEnumerable().ToList();
Perform select on Zones.Id and then check with Contains to get desired result
var zones = DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri)
.Where(z => z.Type == typeof(ZoneDO).ToString() &&
user.UserPreference.Zones.Select(x => x.Id).Contains(z.Id))
.AsEnumerable().ToList();
You can try to use Contain method.
var zones = DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri)
.Where(z => z.Type == typeof(ZoneDO).ToString() &&
user.UserPreference.Zones.Select(x => x.Id).Contain(z.Id)).ToList();
Or you can use inline Where and Count
var zones = DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri)
.Where(
z => z.Type == typeof(ZoneDO).ToString() &&
user.UserPreference.Zones.Where(a=> a.Id == z.Id).Count() > 0
).ToList();
Another option may be to use the Any() operator:
var zones = DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri)
.Where(z => z.Type == typeof(ZoneDO).ToString() &&
user.UserPreference.Zones.Where(x => x.Id == z.Id).Any())
.AsEnumerable().ToList();
Linq providers usually understands basic methods on arrays/collections. Therefore you can use Contains method.
var zones = DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri)
.Where(z => z.Type == typeof(ZoneDO).ToString() &&
user.UserPreference.Zones.Contains(z.Id))
.AsEnumerable().ToList();
So apparently you have a user. This user has a UserPreference and this UserPreference has zero or more Zones. It seems that this user, and the Zones of this user's UserPreference is in local memory (not in a database. The Zones are IEnumerable and not IQueryable)
Although you didn't specify, it seems that DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri) returns an IQueryable<ZoneDO>
You want all ZoneDo that have an Id that is an Id of one of the Zones in the one and only UserPreference of your user.
In baby steps:
IEnumerable<Guid> zoneIds = user.Userpreference.Zones.Select(zone => zone.Id);
IQueryable<ZoneDO> allZoneDOs = DbUtil.Client.CreateDocumentQuery<ZoneDO>(CollectionUri);
IEnumerable<ZoneDO> requestedZoneDOs = allZoneDOs
.Where(zoneDo => zoneIds.Contains(zoneDo.Id);
Until now no query is performed, nor any enumeration. To perform the query and return the fetched data as a List<ZoneDO> use .ToList();
List<ZoneDO> documents = requestedZoneDOs.ToList();
TODO: if desired put all statements into one big LINQ statement. I doubt whether this will improve performance. It surely will deteriorate readability and thus maintainability.
Did you notice that I did not do your type checking part. This was not needed, because function CreateDocumentQuery<ZoneDO> already returns a sequence f ZoneDo.
If not, and there are other types in the returned sequence, use OfType<ZoneDo> instead of checking on string representation of the returned objects:
IQueryable<ZoneDO> allZoneDOs = DbUtil.Client
.CreateDocumentQuery<ZoneDO>(CollectionUri)
.OfType<ZoneDo>();
AsEnumerable is not needed. IQueryable.ToList() will perform the query and convert the data to a list.
AsEnumerable is only needed if you need the data in local memory to continue with LINQ statements that cannot be performed as IQueryable, like LINQ statements where you call local functions, or LINQ statements that cannot be translated into SQL.
AsEnumerable will fetch the requested data per 'page'. So if you only need the first (few) elements, you don't fetch the complete table, but only the first (few) pages.

MongoDB C# Find documents where List<string> contains value of another List<string>

In the process of creating a filtering feature in my mongoDB i have created the following find query using the mongoDB driver for c#.
return await DefaultCollection
.Find(c => c.vintageInfo.IsVintage
&& c.IsPublished
&& c.AverageRating >= lowerRating
&& filterArray1.Contains(c.filterSearchParameters.Dosage))
.Skip(page * pageSize)
.Limit(pageSize)
.ToListAsync();
This is working perfect and it's all great!
Now as we see in the example above i can check if the provided filterArray1 contains the document attribute c.filterSearchParameters.Dosage. Now the problem arises cause, the document further contains a list as follows c.filterSearchParameters.Styles. What i would like to be able to do is check if the list c.filterSearchParameters.Styles contains any value matching filterArray2.
Edit
As per request:
filterSearchParameters looks like:
public class FilterSearchParameters
{
public string Dosage { get; set; }
public List<string> Style { get; set; }
public List<string> Character { get; set; }
}
and the filterArray1 and 2 is just an ordinary:
List<string> filterArray1
List<string> filterArray2
To do this i tried:
return await DefaultCollection
.Find(c => c.vintageInfo.IsVintage
&& c.IsPublished
&& c.AverageRating >= lowerRating
&& filterArray1.Contains(c.filterSearchParameters.Dosage)
&& filterArray2.Any(x => c.filterSearchParameters.Style.Any(y => y.Equals(x))))
.Skip(page * pageSize)
.Limit(pageSize)
.ToListAsync();
Adding the line:
filterArray2.Any(x => c.filterSearchParameters.Style.Any(y => y.Equals(x)))
However this throws Unsopported filter exception. So i tried:
return await DefaultCollection
.Find(c => c.vintageInfo.IsVintage
&& c.IsPublished
&& c.AverageRating >= lowerRating
&& filterArray1.Contains(c.filterSearchParameters.Dosage)
&& c.filterSearchParameters.Style.Intersect(filterArray2).Any())
.Skip(page * pageSize)
.Limit(pageSize)
.ToListAsync();
Adding the line :
c.filterSearchParameters.Style.Intersect(filterArray2).Any()
However the error persisted...
Lastly ive found ->
Builders<Entity>.Filter.ElemMatch(
x => x.filterSearchParameters.Style,
s => filterArray2.Contains(s))
But havn't been able to incorporate it and try it out with the other filters.
Does anyone know how to solve this problem, or any knowledge that could point me in the direction of some documentation i could read, or say if i'm already looking in the wrong place... Any help is appreaciated, thanks in advance.
In your data model Style property is a regular .NET List which can be serialized and deserialized to MongoDB. The drawback is that even though you can see LINQ Extension methods like Any or Intersect, those method cannot be converted into database query. That's why it fails in the runtime.
To match two arrays in MongoDB you can simply use $in operator
You can try below example in Mongo shell:
db.col.save({ a : [1,3,4]})
db.col.find({ a : { $in: [1,2]})
It works in Mongo query language and you have to do the same thing in C#. Because of that Mongo C# driver introduces AnyIn generic method which expects two generic IEnumerable parameters, first one from your data model and second one as a parameter type.
public FilterDefinition<TDocument> AnyIn<TItem>(Expression<Func<TDocument, IEnumerable<TItem>>> field, IEnumerable<TItem> values);
You can use Builders to create such expression:
Expression<Func<YourTypeClass, bool>> p = c => c.vintageInfo.IsVintage
&& c.IsPublished
&& c.AverageRating >= lowerRating
&& filterArray1.Contains(c.filterSearchParameters.Dosage);
var filterBuilder = Builders<YourTypeClass>.Filter;
var filter = filterBuilder.AnyIn(x => x.filterSearchParameters.Style, filterArray2);
return await DefaultCollection
.Find(filterBuilder.And(filter, p))
.Skip(page * pageSize)
.Limit(pageSize)
.ToListAsync();

How do I search a dynamic object IEnumerable<dynamic> using a dynamically built lambda expression?

Previously, I had great help on my previous question, thank you vyrp
,
How do I create and populate a dynamic object using a dynamically built lambda expression
I'm now looking to search the dynamic object, and as before, I don't know the objects properties, and therefore what I'm searching until runtime.
Here's the code that builds the dynamic object:
// Get list of optional fields
var optFieldList = await _tbList_FieldRepository.GetAsync(lf => lf.ListID == listId && lf.DisplayInList == true);
// order list of optional fields
optFieldList.OrderBy(lf => lf.DisplayOrder);
// Get base Data excluding Inactive if applicable
IEnumerable<tbList_Data> primaryData = await _tbList_DataRepository.GetAsync(ld => ld.ListID == listId && (ld.IsActive == includeInactive ? ld.IsActive : true));
// Build IEnumerable<dynamic> from base results plus any optional fields to be displayed in table
var results = primaryData.Select(pd => {
dynamic result = new System.Dynamic.ExpandoObject();
result.Id = pd.ID;
result.PrimaryData = pd.PrimaryData;
result.DisplayOrder = pd.DisplayOrder;
result.IsActive = pd.IsActive;
foreach (var optField in optFieldList)
{
switch (optField.FieldType.ToLower()) {
case "text":
((IDictionary<string, object>)result).Add(optField.FieldName, pd.tbList_DataText.Where(ld => ld.DataRowID == pd.ID && ld.ListColumnID == optField.ID).Select(ld => ld.DataField).DefaultIfEmpty("").First());
break;
}
}
return result;
});
For the purpose of testing, I have 2 dynamic fields, "PhoneNumber" and "FuelType"
I can search the known field(s) i.e. PrimaryData, no problem, as below.
results = results.Where(r => r.PrimaryData.Contains(searchString));
And the following will work if I know the field PhoneNumber at design time
results = results.Where(r => r.PhoneNumber.Contains(searchString));
but what I want to do, is something like:
results = results.Where(r => r.PrimaryData.Contains(searchString)
|| foreach(var optField in optFieldList)
{
r.optField.FieldName.Contains(searchString)
})
ending up with
results = results.Where(r =>
r.PrimaryData.Contains(searchString)
|| r.PhoneNumber.Contains(searchString) ||
r.FuelType.Contains(searchString));
but obviously that code doesn't work. I've tried a bunch of different attempts, none successful, so I'm looking for suggestions. Thanks
Since you know that the dynamic element of your query is actually ExpandoObject, hence IDictionary<string, object>>, you can safely cast it to dictionary interface and use it to access the property values by name, while Enumerable.Any method can be used to simulate dynamic || condition:
results = results.Where(r => r.PrimaryData.Contains(searchString)
|| optFieldList.Any(f =>
{
object value;
return ((IDictionary<string, object>)r).TryGetValue(f.FieldName, out value)
&& value is string && ((string)value).Contains(searchString);
}));

Reuse a LINQ To Entities statement

I have the following Select expression that will be performed database-side:
var model = new ProductListViewModel()
{
Products = products
.Select(q => new ProductViewModel()
{
// Other mapping
Name = q.LanguageKey.LanguageValues.FirstOrDefault(p => p.LanguageKeyID == currentLanguageID && p.Active).Value,
})
.OrderBy(q => q.Name)
};
As you can see, the Name part is quite long, and other part of my program also use the same logic. Therefore, I would like to reuse it (not entire Select statement, just the Name mapping part).
I tried to write a function that only use the entity:
public static string GetProductName(Product product, int languageID)
{
return product.LanguageKey.LanguageValues.AsQueryable()
.FirstOrDefault(q => q.LanguageID == languageID && q.Active).Value;
}
However, on calling the method, I (obviously) receive the error
LINQ to Entities does not recognize the method {X} method, and this method cannot be translated into a store expression
I have searched through many articles on StackOverflow, but most solve the problem of reusing entire Select expression. Is it possible to reuse it? Or do I have to create database Stored Procedure or Function?
One solution is to use LINQKit.
First, you need to modify your method to return an expression like this:
public Expression<Func<Product, string>> CreateExpression(int languageID)
{
return product => product.LanguageKey.LanguageValues.AsQueryable()
.FirstOrDefault(q => q.LanguageID == languageID && q.Active).Value;
}
By the way, I don't think that you need to invoke .AsQueryable() here.
Then you can do the following:
var expression = CreateExpression(currentLanguageID);
var model = new ProductListViewModel()
{
Products = products.AsExpandable()
.Select(q => new ProductViewModel()
{
// Other mapping
Name = expression.Invoke(q),
})
.OrderBy(q => q.Name)
};
Notice the .AsExpandable() call on the products variable.
This method is from LINQKit, and it creates a wrapper around the IQueryable (in your case products) to enable using expressions from inside your lambda expressions.
Also notice the Invoke method that is also coming from LINQKit. It allows you to use the expression inside your other expression, i.e., q => new ProductViewModel(....

Linq Method Error IOrderedQueryable

I have a database with a specific id with recorded Time's, I need help on trying to figure out time gap's between an ID's time's e.g 13:05:15 and 13:05:45 though if the time gap is over 10/15 seconds it needs to be recorded so it can be used in say a text file/other data etc. I previously asked a similar question on here, here is what my code looks like so far:
This class is used to manipulate data through the linq var being queried/looped
public class Result
{
public bool LongerThan10Seconds { get; set; }
public int Id { get; set; }
public DateTime CompletionTime { get; set; }
}
This is the foor loop within a separate class which was my original idea
using (var data = new ProjectEntities())
{
Result lastResult = null;
List<Result> dataResults = new List<Result>();
foreach(var subResult in data.Status.Select(x => x.ID).Distinct().Select(Id => data.Status.Where(x => x.ID == Id).OrderBy(x => x.Time)))
{
if (lastResult != null)
{
if (subResult.CompletionTime.Subtract(lastResult.CompletionTime).Seconds > 10)
dataResults.Add(subResult);
}
lastResult = subResult;
}
Which I got the error:
Linq.IOrderedQueryAble does not contain a definition for 'CompletionTime' and no Extension method 'CompletionTime' accepting a first argument of type 'System.Linq.IOrderedQueryable.
I changed the for loop to use an object of the manipulation class
foreach(Result subResult in data.AssetStatusHistories.Select(x => x.ID).Distinct().SelectMany(Id => data.AssetStatusHistories.Where(x => x.ID == Id).OrderBy(x => x.TimeStamp)))
{
if (lastResult != null)
{
if (subResult.CompletionTime.Subtract(lastResult.CompletionTime).Seconds > 10)
{
vehicleResults.Add(subResult);
}
}
lastResult = subResult;
}
Though now I get the error: Cannot convert type 'Project.Status' to 'Project.Result'
Does anyone possibly have a solution to get around this I have looked through a few resources but haven't been able to find anything of helps also even on Microsoft's Linq Forum. Any help is much appreciated ! :)
Try adding .ToList() to the end of your LINQ statement, after OrderBy:
var results = data.Status.Select(x => x.ID).Distinct()
.Select(Id => data.Status.Where(x => x.ID == Id)
.OrderBy(x => x.Time)
.ToList();
foreach(var subResult in results))
{
...
}
Also, I think you could modify your LINQ to do a GroupBy of the ID column, but that's something you could do research on if you wish. (Tutorial)
Your linq query (in the second try) will return an IEnumerable of whatever is the element type of data.AssetStatusHistories. I assume this is some kind of IEnumerable<Project.Status>, so you're in fact trying to assign Project.Status objects to an iterator variable (subResult) of type Result, which is why you're getting the error Cannot convert type 'Project.Status' to 'Project.Result'.
So your problem is not really in the linq query, you just need a way to convert your Project.Status objects to Project.Result objects.

Categories

Resources