[CSHARP-456] Support linq projections to only pull back referenced fields. Created: 27/Apr/12  Updated: 04/Apr/15  Resolved: 04/Apr/15

Status: Closed
Project: C# Driver
Component/s: None
Affects Version/s: 1.4.2
Fix Version/s: None

Type: New Feature Priority: Minor - P4
Reporter: Craig Wilson Assignee: Craig Wilson
Resolution: Done Votes: 53
Labels: C#, linq
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Issue Links:
Depends
depends on CSHARP-601 Linq to Aggregation Framework Closed
Related
related to CSHARP-912 Support SelectMany where subset comes... Closed

 Description   

It should only pull back fields required for projection. In addition, it should support these forms
1) simple projections: x.FirstName
2) complex projections: x.FirstName + " " + x.LastName
3) anonymous projections: new { Name = x.FirstName + " " + x.LastName
4) class Projections: new PersonName(x.FirstName, x.LastName)

{ Age = x.BirthDate.ToAge() }

);



 Comments   
Comment by Craig Wilson [ 04/Apr/15 ]

This has been implemented in the 2.0 driver and, when LINQ is rewritten, CSHARP-601, it will be fully realized.

Comment by Olivier Ducros [ 04/Dec/13 ]

Same problem here, should not be a minor bug, the problem is hidden and unexpected.

Comment by Adam Baldwin [ 08/Oct/13 ]

Same here, this is killing our ability to use linq queries on large documents

Comment by Pete Smith [ 24/Sep/13 ]

Will do... let me know if there's anything I might be able to assist with.

Comment by Craig Wilson [ 24/Sep/13 ]

You'll want to track CSHARP-601.

Comment by Pete Smith [ 24/Sep/13 ]

Any updates on this one? Would be really useful for us!

Comment by Michael Kennedy [ 16/Jul/13 ]

Hi Daniel and Craig,

Thanks for the update Craig. Sounds like it's on the right track!

Comment by Craig Wilson [ 16/Jul/13 ]

Mike and Daniel,
This is exactly what the rewrite does. You can see the current work here: https://github.com/craiggwilson/mongo-csharp-driver/tree/csharp601. It is about 90% done. There are a couple things to know about it. It supports Aggregation Framework natively, although that has to be optet-in. In every linq query, both query based and Aggregation Framework based, the final projection will always be applied client-side. However, we inspect that expression and only pull back the fields that are necessary to do the client-side projection. You'll see we can do some extremely complex projections client-side as long as we provide a way to drop in just the necessary fields (https://github.com/craiggwilson/mongo-csharp-driver/blob/csharp601/MongoDB.DriverUnitTests/Linq/Translators/Methods/SelectTests_Query.cs#L271).

As far as partially populated entities, that will likely not ever happen. The only way it would get included is if we supported proxy types to lazy-load the remaining fields. That is a wholely different discussion. So, the solution is for the user to create "Summary" objects that contain exactly what they need and project into them.

Comment by Daniel Sinclair [ 16/Jul/13 ]

Hey Mike, yep. This is where I am too. Come to think of it, I wonder what's in store for the Linq/Aggregation support Craig? Presumably that will also require dynamic entities or suffer partially populate POCOS. I ran into this last year and ended up returning the aggregated queries as JSON rather than serializing through POCO but it wasn't pretty because that meant no language support for the queries (which also had to be converted into JSON).

Overall, I think partially populated POCOs are a neat solution for the web, and I don't see a problem with the driver returning partial entities as long as ALL those fields are nullable. That's something that could be easily verified during the projection, right?

Comment by Michael Kennedy [ 16/Jul/13 ]

Hi,

Glad to see this is being addressed.

Given the complexity of supporting any projection whatsoever, could you start by doing this.

1. For SIMPLE projections, do the projection in mongo
2. For more complex ones, fall back to what you're doing now (projections on the client).

So for example, it seems odd that this would return the full document and then pull out the one field:

var activeUserNames = ctx.Users.Where(u => u.IsActive).Select(u => u.Name).ToArray();

Surely these types of queries are very common and perf would go up if you supported them.

As an example, I have a bunch of docs each around 10K. I just wanted a list of IDs kind of like above. Apparently we are pulling back tons of data just to get a list of integers.

Thanks,
Michael

Comment by Daniel Sinclair [ 12/Jul/13 ]

Thanks Craig. Comments noted. I've only compiled the code above with
notepad.exe so far, but the IQueryable<T> consumer is a web client as I'm
handing it out directly through ASP.Net Web API, which means Json.Net
serialization. I'm expecting that to work as expected, but I take your
point about not being able to "do any further filtering on iq". If that's a
problem, I guess I'll find out pretty quickly as soon as something that
consumes my All() function tries to do that. Off the top of my head, I'm
pretty sure I DON'T do that yet and all the Where() clauses are bundled
into the original query for passing through to the driver, hence the reason
for this. But I guess I'll find out soon enough if there's a
non-serialization consumer.

Comment by Craig Wilson [ 11/Jul/13 ]

It is still IQueryable<T>. Otherwise, you wouldn't be able to assign it back the same iq variable. However, this isn't going to go through the normal serialization machinery anymore and will return a T with just the Id field populated, and then we are in a similar boat as described above with semi-populated entities. I can't stop you from doing this so you could probably figure out someway to work this in as an extension method that takes just field names and preserves the return type.

Now, at this point in your query, you can't do any further filtering on iq though without this going into the Aggregation Framework. There are circumstances when this would still be possible, but it would be extremely difficult to get right and lots of edge cases exist, so that won't be in the initial version of the new linq provider.

Comment by Daniel Sinclair [ 11/Jul/13 ]

That's a very fair point about spliced Person types Craig. I can understand why you wouldn't want to do that. For me, I'm basically working in Json at both ends and there's lots of assumed flexibility. Almost every field is nullable and hence stripped out on the wire anyway. In fact I wonder sometimes why I'm (de)serializing into .Net at all. However, .net is a nice place to write some complex code and the strict type safety means getting rid of all magic strings. So Linq expressions are a perfect way not only for refactoring tool support but also for building a repository pattern with a common interface that would work on various DB providers, not least a unit test provider.

We can already cope with partial updates, so a partial Person is turned into a pattern of $set type operations with the help of [BsonIgnoreIfNull] and I can do similar things to clear fields with $unset.

Interesting comment about there being no MongoCursor, but I guess I'm going to need to construct a Where() clause somehow myself, but at least I can now do field restruction with that projection support you've put into this build.

Your code example has got me thinking again though. So in my previous stab;

IQueryable<T> iq = MongoDB.Driver.Linq.LinqExtensionMethods.AsQueryable(Collection);
iq = iq.Where( .. some criteria ..);
// bring back Id field only
iq = iq.Select(e => new T() { Id=e.Id } );

What type is iq returning in the above case? Is it still type compatible with IQueryable<T>? Or is that all down to serialization...in which case I'm fine I guess as long as the fields I'm skipping are nullable.

Comment by Craig Wilson [ 11/Jul/13 ]

Yeah, but the problem with projections is that restricting fields also changes the type. For instance, what does it mean to have a full Person entity with only the Id and Name field populated? That could be very misleading to the consumers of your code, which of course is your problem and not really ours (except that we have now enabled that to happen and will subsequently get questions about). In other words, if I have an instance of a Person, do I have the full thing or just a projected version? Regardless, the side-band kinda thing you are referring to would be some extension methods you'd probably cook up yourself which would, underneath, build up a Tuple expression. For instance:

public virtual IQueryable<T> All(string[] fieldNames, params Expression<Func<T, bool>>[] rules)
{
    IQueryable<T> iq = MongoDB.Driver.Linq.LinqExtensionMethods.AsQueryable(Collection);
 
    // Add in security and criteria
    iq = iq.Where(GetSecureReadExpression());
    iq = iq.Where(ExpressionHelpers.CombinePredicateExpressions(rules));
 
    // ONLY DREAMING!!
    IQueryable<Tuple<ObjectId, string, int>> fieldRestrictedIQ = iq.Select<ObjectId, string, int>("Id", "Name", "Age");
 
    return iq;
}

There would obviously need to be some type knowledge here which is what I was referring to above, but this is possible. This particular extension method would build an expression tree. You could have N number of these for the varying cardinality necessary. You could also go without types and return a BsonDocument or Dictionary<string, object>, but then your application would have to figure out how to decipher what this means.

Even worse for us is that if you pull back a partially reconstituted Person instance, then what happens when the user calls Save(person)? It would overwrite all the data that wasn't pulled back with the default values in the entity. This would be horrible.

I guess in summary, we aren't going to put anything like this in the box and you're gonna have to write some code on your own to make this work like want. The hook that is there for you to do this is that Select takes an Expression which you can construct yourself.

BTW: We can't provide access to the underlying MongoCursor because it doesn't exist. Particularly because the next version of the Linq provider will also be support Aggregation Framework which doesn't use a cursor to begin with.

Comment by Daniel Sinclair [ 11/Jul/13 ]

Actually, it's a little simpler than this. Everything I've done so far above is a means to an end. I'm perfectly happy with strongly typed linq queries. I don't need/want dynamic queries any other way. Far from it. It's just the field restriction that I need. I don't need that part to be in Linq but as per the support mentioned in this ticket it would be the only way to get the server to filter fields and return a streaming IQueryable.

I think projection via Select() is the right thing to do for the Linq driver, but I'd be perfectly happy with any other mechanism that allowed me to trim the resultset, however side-band it might be. For instance, if there was some other way to influence the underlying MongoCursor rather than construction an Expression just to feed into the Linq Select().

This is probably where I'd like to be;

        public virtual IQueryable<T> All(string[] fieldNames, params Expression<Func<T, bool>>[] rules)
        {
            IQueryable<T> iq = MongoDB.Driver.Linq.LinqExtensionMethods.AsQueryable(Collection);
 
            // Add in security and criteria
            iq = iq.Where(GetSecureReadExpression());
            iq = iq.Where(ExpressionHelpers.CombinePredicateExpressions(rules));
 
            // ONLY DREAMING!!
            iq.GetMongoCursor().SetFields(fieldNames);
 
            return iq;
        }

I don't expect that Expression building stuff is going to work out well with types, but if I can limit the scope of fields that restriction can be used with (which I can), I could do this ugly mess;

foreach (var fieldName in fieldNames)
{
 iq.Select(getFieldProjection(fieldName);
}

where

Expression<Func<T, bool>> getFieldProjection(string fieldName)
{
 switch(fieldName)
 {
   case "Id": return e => new T() { Id=e.Id };
   case "Name" : return e => new T() { Name=e.Name };
   default: throw ...
 }
}
}

Google hangout is good with me with this email account.

Comment by Craig Wilson [ 11/Jul/13 ]

Gotcha. Yeah, you are getting into territory explored by many who are attempting to use Linq. Linq requires typing and therefore you are going to have to resort to either building up expressions manually (as you did above) or use something else that is Linq-ish. For instance, Microsoft has a sample called Dynamic Linq (http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx) that is more flexible but possibly does what you are looking for. Is very possible to do something like this with a little effort building a parser:

var queryable = collection.AsQueryable().Where("Age > 23").Select(t => new Tuple<ObjectId, string>(t.Id, t.Name));

I've obviously put the Age into a string, which could arguable come from some user provided value. That is relatively easy. The difficult part is the projection piece. Because you are wanting to use Linq, you need a strongly typed result and having the user provide arbitrary fields which could be of any type makes that an extremely difficult problem. It feels like you need to be working with dynamic types and not Linq. We will have support for dynamic types with version 2.0 of the driver.

So even with the resolution of this ticket, you are going to need to do some pretty crazy stuff to get what you want as it is unlikely we'll be providing untyped Linq-ish behavior.

BTW: I'd also like to note that it is sometimes difficult to get performance out of MongoDB when you are allowing your users to provide arbitrary query expressions. This is because you'd need to know all the possible permutations of things the user would do and create an index for each of them. This might be possible on small documents, but for large documents, things start to get hairy.

If you'd like, I'm happy to jump on a google hangout and talk this out with you.

Comment by Daniel Sinclair [ 11/Jul/13 ]

Thanks Craig
Yes, so I'm mainly using Linq declaratively for expressions, but there are cases where I want to trim the resultset with a filter from an external source, which is a string field name. The Expression building stuff is just Linq true - I'm just trying to work out how I can hook into your provider effectively.

Your comment about not knowing the types is very troubling.

The .Include trick is what I'm doing/abandoned today, but unless I've missed something (quite probably) doesn't that require a cursor? I still want to return IQueryable<>. The trouble is that AsQueryable<> on the cursor (as does your foreach I think) fetches all the data because it's the standard Linq one working over IEnumerable?, even though the .Include takes place on the server I believe.

So what I'm trying to do is this

  • use linq for query
  • pass the name of a string field for restriction/projection (without knowing types) that filters server-side
  • return MongoDB.Driver.Linq.LinqExtensionMethods.AsQueryable
Comment by Craig Wilson [ 11/Jul/13 ]

Hi Daniel,
Linq is about typed lambda expressions getting turned into some underlying query. What you are demonstrating above isn't really typed as you don't really know until runtime what you actually want to select out. So yes, your solution above by building your own expression tree would be the way to do this. This, BTW, is also how you'd need to do this for any other linq provider as well. One of the flaws with this approach is that you need to know the types of each of the field, and all you have is their field name. This is going to make figuring out the return type (Tuple<>) very difficult. If you can make assumptions (like all fields are of type string) as you did in your example, then that isn't much of a problem.

Just to point out, if you want to mix Linq for predicates and still use something like this, you can do something like this today.

  var collection = db.GetCollection<BsonDocument>();
  var fields = Fields.Include("_id").Include("Name");
  var query = Query.Where<T>(t => t.Age > 23);
 
  foreach(BsonDocument doc in collection.Find(query).SetFields(fields))
  {
    // do something in here...
  }

Comment by Daniel Sinclair [ 11/Jul/13 ]

What's the optimal way hook into this if I have string field names as opposed to early bound?

var fields[] =

{"Id","Name"}

;

Am I going to have to build up an Expression.Lambda to pass into Select() by hand?

eg. something like this?

var projection = Expression.Lambda<Func<T, Tuple<string, string>>>(
Expression.Call(typeof(Tuple), "Create", new[]

{typeof(string), typeof(string)}

,
Expression.PropertyOrField(param, "Id"),
Expression.PropertyOrField(param, "Name")), param);

Ick.

Comment by Craig Wilson [ 10/Jul/13 ]

Awesome, thanks so much. Yes, that is it and, in this case, it should only bring back the Id field. You shouldn't need to do anything special for that to happen automatically.

Comment by Daniel Sinclair [ 10/Jul/13 ]

I'd like to be putting this through it's paces, but I'm missing the syntax. I want to use field restriction on the server with the linq syntax.

Did I miss an example code snippit somewhere? I'm guessing it will be something like this, but I want to make sure I have the right IQueryable and I'm not accidentally doing it in memory (especially since the MongoDB log doesn't reflect field-restriction so it's not easy to tell);

IQueryable<T> iq = MongoDB.Driver.Linq.LinqExtensionMethods.AsQueryable(Collection);

iq = iq.Where( .. some criteria ..);

// bring back Id field only
iq = iq.Select(e => new T()

{ Id=e.Id }

);

Comment by Craig Wilson [ 22/May/13 ]

Ok guys... The branch I posted above (https://github.com/craiggwilson/mongo-csharp-driver/tree/csharp601) has the current version of this completely on top of master; it is completely up-to-date. I'd consider this ~90% complete where there are some known things lacking (like $elemMatch in a projection) and there are probably some bugs. However, all our current tests pass and all the existing linq expressions from the old library are still supported. This should be a drop-in replacement for the old queries.

That being said, this isn't released because I'm quite sure there are a few bugs related to edge cases. Since you all seem so eager, it'd be a great help to put this through the ringer and let me know of things that should work that don't and things that shouldn't work that do (for instance, we send an query to the server it can't process, but no error comes back). You can just pull this branch, build it, and use it instead of the official libraries.

Hope that is enough for now... we are targeting this for the next major/minor version. As in, if we release a 1.9, then this will be in it. Otherwise, it will be in 2.0. We are doing lots of other things right now in addition to this, so it is taking a bit longer than we wanted.

Comment by Daniel Sinclair [ 22/May/13 ]

+1

Comment by Zaid Masud [ 22/May/13 ]

I agree with Avishay here. Since it's taking so long to get to the full solution, an interim FluentMongo-like solution, even supporting very basic projections, will be very valuable for driver users.

Comment by Avishay Lavie [ 22/May/13 ]

Since LINQ to AF seems like a pretty big effort, maybe there's value in providing partial support for simple projections, using the approach outlined above of instantiating the document class and only filling in the required properties/fields (a-la FluentMongo), and falling back to the current solution of client-side projection if the document class has a constructor map or ISupportInitialize or anything else that might get in the way. I think this should cover a lot of cases with relatively low cost. Any chance of that happening? If someone were to contribute a pull request that does this, would you pull it?

Comment by Daniel Sinclair [ 15/May/13 ]

Is there a relatively stable codebase for projection with IQueryable<> yet?

Comment by Craig Wilson [ 05/Mar/13 ]

No, no production ready version with these changes. This is still a ways off from being fully baked.

Comment by Daniel Sinclair [ 05/Mar/13 ]

Oh - I thought this was the master branch? Is there a production ready
version with these changes?

Comment by Craig Wilson [ 05/Mar/13 ]

Be careful (and don't use in production). This is unreviewed code that is far off master at this point. It won't get pushed back into master for a little while.

Comment by Daniel Sinclair [ 04/Mar/13 ]

pulled it - will do!

Comment by Craig Wilson [ 04/Mar/13 ]

I will work for both normal queries and aggregation queries.

Comment by Adam Baldwin [ 27/Feb/13 ]

So, will this feature work for normal queries too or just aggregated queries? I'm facing the same situation where I have a massive subdocument property that I don't want to pull into memory (and it's not part of my mapped class). My linq statement can't, unfortunately just be injected into the MongoCollection because they have paging and sorting expressions as well.

Comment by Craig Wilson [ 10/Jan/13 ]

I have this mostly working, but it is tied up in the work needed to support linq to Aggregation framework. I'm currently working on getting GroupBy's translated into the $group pipeline element. You can see the current work here... https://github.com/craiggwilson/mongo-csharp-driver/tree/csharp601/MongoDB.Driver/Linq. It hasn't been cleaned up yet, so there is likely some cruft hanging around.

Comment by Chris Robison [ 10/Jan/13 ]

I'm working on a project where documents might get big. I'd like to contribute to this if I can. Is there any place I can help?

Comment by Daniel Sinclair [ 10/Dec/12 ]

Actually, I wasn't reading it right. The query restriction doesn't show up in the log, but having looked directly in the buffer being sent/returned from Mongo (I couldn't use NetMon because I'm on localhost) it appears to be doing the right thing.

So for my case at least, this is a usable workaround. I appreciate this may not be the general case, but it seems to get me out of a pickle!

Comment by Daniel Sinclair [ 10/Dec/12 ]

I'm looking forward to the Aggregation integration, that will be
tremendously beneficial, and so of course you're right I just tried
instead of this;

return this.Collection.AsQueryable<T>()
.Where(linqExpression);

using this;

var mc = this.Collection.FindAs<T>(Query.Where(linqExpression));
mc.Fields = Fields.Include(fieldNames);
return mc.AsQueryable();

And although it works, and I was initially excited, it clearly isn't
translating that field expression to the Mongo. It's still fetching the
whole document but I only get back the fieldNames I asked for. Which makes
me wonder if a MongoCursor works the way I'd expected. I would have thought
that setting the Fields property on the cursor would have constructed the
appropriate query but it must be doing this in the driver?

Comment by Craig Wilson [ 10/Dec/12 ]

No, it's not that simple, although that would be nice. There is currently no workaround for pulling back only required fields in a Linq query. It is a problem with sucking the mapping information out the BsonClassMaps and applying them to another class, for instance, an anonymous class. This transference is rather difficult when stuff like default values start to occur. By making the class maps and member maps so flexible and feature rich, it caused supporting pulling back only the required fields to be especially difficult.

So, my apologies, but there is no workaround. I'll get linq to aggregation done soon which will encompass this functionality and all will be right with the world.

Comment by Daniel Sinclair [ 10/Dec/12 ]

Oh wait, is it as simple as using a Mongo Cursor (instead of Collection.AsQueryable()) and then calling AsQueryable() on the cursor?

Comment by Daniel Sinclair [ 10/Dec/12 ]

Sorry, bad terminology. I'm just trying to do a Fields.Include() with my
query. I can a do this on MongoCursor obviously, but I'm trying to see
where I can inject similar functionality into the query stream when I'm
using LinqExtensionMethods.AsQueryable() as per my Repository.

Basically, I'm just trying to bring back only those fields I ask for, NOT
the whole document. So it's a Projection really I guess, but a field filter
is how I was thinking about it.

Comment by Craig Wilson [ 10/Dec/12 ]

This ticket is about projections, not filters. Could you clarify what you mean by filters? Perhaps some sample code would help as well showing what you want to do...

Comment by Daniel Sinclair [ 10/Dec/12 ]

Assuming a Repository pattern, can you think of a workaround in the iterim?
Is there a good place to inject the necessary filters into the query?

I've done this for a single document because I used a MongoCursor, but I'd
like to do this with AsQueryable()

Comment by Craig Wilson [ 10/Dec/12 ]

Maybe 1.8, but probably not. I'm currently working on Linq to Aggregation and this particular feature is mostly working, but the rest of the aggregation stuff isn't finished. Hence, this will happen when Linq to Aggregation is finished.

Comment by Daniel Sinclair [ 10/Dec/12 ]

Any thoughts on when this feature might make it into a release?

Comment by Craig Wilson [ 10/Nov/12 ]

Implementing Linq to Aggregation will require a generic way to project actual fields, making this ticket relatively trivial or unnecessary once linq to aggregation is complete.

Comment by Craig Wilson [ 13/Sep/12 ]

I think we are saying the same thing. When a projection happens, then "ISupportInitialze" will not be run because it doesn't apply. But to accomplish that requires a different container to project out of rather than the mapped class (which might have some dependencies on ISupportInitialize to work correctly). Therefore, we have to find the fields needed for projection and then rewrite the expression trees to deserialize a mapped member into a different container than was originally intended (the mapped class), i.e. copying the serialization information for that member onto a different member.

I've actually spiked this out, and then went further to attempt supporting SelectMany as well, but that becomes extremely difficult to get right. Therefore, we'll probably just support Select to begin with. However, we are also looking at integrating Aggregation Framework queries into the linq provider, which will probably require a lot of rework and this ticket would probably get absorbed into that rewrite.

Comment by Zaid Masud [ 13/Sep/12 ]

Pardon my ignorance, but if a .Select clause is specified, is there really no way to bypass the class instantiation? I suppose this may be what you are alluding to when you mention container projection?

We would be able to find the corresponding field names form the class map / elements, construct the query, and then return only the values required in the .Select.

As a driver user I personally would have no expectation of ISupportInitialize still being supported if I was explicitly specifying a .Select clause.

Comment by Craig Wilson [ 13/Jun/12 ]

FluentMongo did this, yes. It worked by pulling back only the fields required for the projection, but still instantiated the whole class. So, if I had a class called Person with a FirstName and LastName property and did a query with a projection only for FirstName, we'd still instantiate the whole Person class and then do a local projection off of that. It works, but since we support things like ISupportInitialize, then instantiating a class with partial data could prove problematic. Therefore, we are going to project into some other container and rewrite the projection tree to come from the alternative container. Much more difficult, but definitely more correct.

Comment by Zaid Masud [ 13/Jun/12 ]

Craig – am I mistaken in saying that Fluent Mongo did this quite well?

Comment by Craig Wilson [ 04/May/12 ]

Yes, it is a bit misleading that we support select, but still pull back the entire document. Projections are rather difficult and we are working through the best way of handling them right now. If you'd like this functionality, please vote it up so we have a feel of the communities need for this support.

Comment by CP Digger [ 04/May/12 ]

We would like to have the first option ( 1) simple projection)
In our program we have very large documents (Images) and we want only retrieve the ObjectId's of the document.

IQueryable<ObjectId> queryable = collection.AsQueryable<Image>().OrderByDescending(img => img.Created).Skip(0).Take(15).Select(img => img._id);

The above code performs very slow and memory consuming. The problem is that in Projection<TSource, TResult> class the "GetEnumerator()" method don't call _cursor.SetFields() restriction and the complete document was loaded from the database to the client

If we implement it directly with MongoCursor the code looks like this :

MongoCursor<Image> cursor = collection.FindAll();
cursor.SetSortOrder(SortBy.Descending("Created"));
cursor.SetSkip(0);
cursor.SetLimit(15);
cursor.SetFields(Fields.Include("_id")); // !!! restriction
foreach (Image item in cursor) yield return item._id;

Generated at Wed Feb 07 21:36:53 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.