MongoDB .NET driver find all : How to write it better? - c#

I am able to query all the items of a collection using 2 approaches
a)
var findAll = await Context.ItemsCollection.FindAsync(_ => true);
var res = await findAll.ToListAsync();
b)
var res = await.Context.ItemsCollection.Find(_ => true).ToListAsync();
Is there a real difference between them?Which one should I prefer ?

There's no real difference. It will eventually behave the same.
Find doesn't execute the query, while FindAsync does, but using ToListAsync makes that difference irrelevant.

Related

Entity Linq find index before query is executed

I have the following query:
var result = //some Entity framework IOrderedQueryable;
var fullList = await result.ToListAsync();
var currentIndex = fullList.FindIndex(x => x.Id== model.Id);
var nextRecord = fullList[currentIndex + 1];
This code works and does what I need.
The problem is, if the full list is thousands of entries, I have to pull the whole thing into memory to find the one item I am looking for.
Is there a more efficient way I can achieve this?
As a note, there are some dynamic where clauses applied to the Queryable, so it has to be done like this, rather than, for example, writing in SQL

Find then Sum with IMongoCollection

I am writing an api to sum a column called ViewCount from collection. At the moment, my code is something like this:
var filter = Builders<Post>.Filter.Eq(u => u.IsDelete, query.Param.IsDelete);
IMongoCollection<Post> _posts;
var postViewCount = _posts.Find(filter).ToList().Select(a =>a.ViewCount).Sum();
Result is ok with correct summation but performance is getting slow a lots with only 20k records (about 6s for simple call). If I use like this, it will be very faster (just 200ms) but I cannot put filter in here:
var filter = Builders<Post>.Filter.Eq(u => u.IsDelete, query.Param.IsDelete);
IMongoCollection<Post> _posts;
var postViewCount = _posts.AsQueryable().Sum(x => x.ViewCount);
So my question is how can I handle this case? Thanks guys!
Use .Where(). After calling .AsQueryable() you get IQueryable, so you have all LINQ funtionality available. :)
_posts.AsQueryable().Where(filter).Sum(x => x.ViewCount);
If you use the Agg feature, you will get a faster query.
Good luck with!
var aggregate = new BsonDocument
{
{ "Count", new BsonDocument("$sum", 1) }
};
_posts.Aggregate().Match(filter).Group(aggregate);

Using EF Include on TPH

I have implemented code first db architecture with simple inheritance using THP:
And I need to query all notifications of all type.
TargetUser property in NotificationUser table is a association.
I'm trying to execute next code:
var notifications = _context.Notifications;
foreach (var notification in notifications)
{
Debug.WriteLine((notification is NotificationUser)? ((NotificationUser) notification).TargetUser?.Name : "-");
}
In database propety TargetUser is set to correct foreign key, but in code I don't get any result. Lazy loading is enabled.
Is it possible to user eager loading? I had already tried to write _context.Notifications.Include('TargetUser') byt it throws an exception.
Upd. The exception is:
A specified Include path is not valid. The EntityType 'Core.Concrete.NotificationBase' does not declare a navigation property with the name 'TargetUser'.
Tried to modify this answer to:
var notifications = _context.Notifications.OfType<NotificationUser>()
.Include(n => n.TargetUser)
.Cast<NotificationBase>()
.Union(_context.Notifications.OfType<NotificationPlace>()
but still the same exception is thrown.
I know this is an old thread, but I'd still like to post some improvements for somebody looking for the same solution.
1. Network Redundancy
Selecting Ids and then running a query, that loads items with the Ids is redundant and the same effect can be achieved by simply running this
Solution:
var userNotifications = _context.Notifications
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.OfType<NotificationUser>()
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToList();
That way, you aren't waiting for 2 DB connections, but just one. Also you save some traffic.
2. Paging on ignored entities?
One would assume, that this specific method is used for only viewing Entities of an inherited type so I would expect Skip and Take to work directly on only entities of said type. e.g. I want to skip 10 NotificationUsers, not 10 Users (of which only 4 are NotificationUsers for example).
Solution: Move ofType higher up the query
var userNotifications = _context.Notifications
.OfType<NotificationUser>()
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToList();
3. Async/Await
When writing an API, you should think about using async/await as that doesn't block the thread thus wastes less resources (this will probably require you to rewrite a lot of your existing code if you don't use it already though).
Please study the advantages of async/await and use them in scenarios like waiting for a result.
Solution: Change this
private List<NotificationUser> GetNotificationUsers(int offset, int limit)
{
return _context.Notifications
.OfType<NotificationUser>()
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToList();
}
into this
private async Task<List<NotificationUser>> GetNotificationUsersAsync(int offset, int limit)
{
return await _context.Notifications
.OfType<NotificationUser>()
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit)
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.ToListAsync();
}
NOTE:
You also then have to change any place that uses this method from
var x = GetNotificationUsers(skip, take);
to
var x = await GetNotificationUsersAsync(skip, take);
And make that method async and return a task as well
I don't know about the amount of entities you will work with. If possible, I would try to do the union not on the DB server:
var userNotifications = _context.Notifications.OfType<NotificationUser>()
.Include(n => n.TargetUser).ToList();
var placeNotifications = _context.Notifications.OfType<NotificationPlace>().ToList();
var notifications = userNotifications.Union(placeNotifications);
See https://stackoverflow.com/a/27643393/2342504
Already tried a lot of different solutions, and none fit my requirements, as I'm working on an API, and the query must support pagination and make constant requests to the database (and not loading all entities in memory).
Finally found out a solution, perhaps not the best, but enough for now.
Firstly, I request a portion of ordered data (pagination logic):
var notifications = _context.Notifications
.OrderByDescending(n => n.DateTime)
.Skip(offset)
.Take(limit);
(At this point I'm not interested in any properties) Next, I'm getting the Id's of loaded items for each entity type:
var ids = notifications.OfType<NotificationUser>().Select(n => n.Id).ToList();
and lastly load specific entities including all properties:
var userNotifications = _context.Notifications.OfType<NotificationUser>()
.Include(n => n.TargetUser)
.Include(n => n.TargetUser.Images)
.Where(n => ids.Contains(n.Id))
.ToList();
all entities goes to list and sorted one more time.
A lot of bad stuff here, hope someone can provide better solution.
If I understood well, you want to get all the entities from the table + the property TargetUser when relevant (for entities of type NotificationUser). You said that "Lazy loading is enabled", which is a good thing.
You tried something like (updated to latest version of C#):
var notifications = _context.Notifications;
foreach (var notification in notifications)
{
Debug.WriteLine((notification is NotificationUser notificationUser)
? notificationUser.TargetUser?.Name
: "-");
}
If you don't get any result, it probably means that your entities are badly configured to work with lazy loading:
Are your classes public?
Are your navigation properties defined as public, virtual?
See: https://learn.microsoft.com/en-us/ef/ef6/querying/related-data#lazy-loading

Getting a single object from mongodb in C#

I've picked up a piece of code that is using the MongoDB driver like this to get a single object from a collection...this can't be right, can it? Is there a better way of getting this?
IMongoCollection<ApplicationUser> userCollection;
....
userCollection.FindAsync(x => x.Id == inputId).Result.ToListAsync().Result.Single();
Yes, there is.
First of all don't use FindAsync, use Find instead. On the IFindFluent result use the SingleAsync extension method and await the returned task inside an async method:
async Task MainAsync()
{
IMongoCollection<ApplicationUser> userCollection = ...;
var applicationUser = await userCollection.Find(_ => _.Id == inputId).SingleAsync();
}
The new driver uses async-await exclusively. Don't block on it by using Task.Result.
You should limit your query before executing, otherwise you will first find all results and then only read one of it.
You could either specify the limit using FindOptions in FindAsync, or use the fluent syntax to limit the query before executing it:
var results = await userCollection.Find(x => x.Id == inputId).Limit(1).ToListAsync();
ApplicationUser singleResult = results.FirstOrDefault();
The result from ToListAsync will be a list but since you limited the number of results to 1, that list will only have a single result which you can access using Linq.
In newer versions of MongoDB Find() is deprecated, so you can either use
collection.FindSync(o => o.Id == myId).Single()
or
collection.FindAsync(o => o.Id == myId).Result.Single()
You can also use SingleOrDefault(), which returns null if no match was found instead of throwing an exception.
I could not get the method:
coll.Find(_ => _.Id == inputId).SingleAsync();
To work as I was getting the error
InvalidOperationException: Sequence contains more than one element c#
So I ended up using .FirstOrDefault()
Example:
public FooClass GetFirstFooDocument(string templatename)
{
var coll = db.GetCollection<FooClass>("foo");
FooClass foo = coll.Find(_ => _.TemplateName == templatename).FirstOrDefault();
return foo;
}

How to do mongodb queries faster?

I have lots of queries like sample1,sample2 and sample3. There are more than 13 million records in mongodb collection. So this query getting long time. Is there any way to faster this query?
I think using IMongoQuery object to resolve this problem. Is there any better way?
Sample 1:
var collection = new MongoDbRepo().DbCollection<Model>("tblmodel");
decimal total1 = collection.FindAll()
.SelectMany(x => x.MMB.MVD)
.Where(x => x.M01.ToLower() == "try")
.Sum(x => x.M06);
Sample 2:
var collection = new MongoDbRepo().DbCollection<Model>("tblmodel");
decimal total2 = collection.FindAll().Sum(x => x.MMB.MVO.O01);
Sample 3:
var list1= collection.FindAll()
.SelectMany(x => x.MHB.VLH)
.Where(x => x.V15 > 1).ToList();
var list2= list1.GroupBy(x => new { x.H03, x.H09 })
.Select(lg =>
new
{
Prop1= lg.Key.H03,
Prop2= lg.Count(),
Prop3= lg.Sum(w => w.H09),
});
The function FindAll returns a MongoCursor. When you add LINQ extension methods on to the FindAll, all of the processing happens on the client, not the Database server. Every document is returned to the client. Ideally, you'll need to pass in a query to limit the results by using Find.
Or, you could use the AsQueryable function to better utilize LINQ expressions and the extension methods:
var results = collection.AsQueryable().Where(....);
I don't understand your data model, so I can't offer any specific suggestions as to how to add a query that would filter more of the data on the server.
You can use the SetFields chainable method after FindAll to limit the fields that are returned if you really do need to return every document to the client for processing.
You also might find that writing some of the queries using the MongoDB aggregation framework might produce similar results, without sending any data to the client (except the results). Or, possibly a Map-Reduce depending on the nature of the data.

Categories

Resources