Search code examples
c#mongodbprojection

Pass-through projection definition for MongoDB in C#


I have a function I use to retrieve an object by its ID from Mongo DB and it looks like this:

public async Task<IEnumerable<TItem>> GetItemsAsync(uint limit, 
    Expression<Func<TItem, bool>> filter = null,
    SortDefinition<TItem> sort = null, 
    ProjectionDefinition<TItem> projection = default, 
    CancellationToken token = default) {

    // First, create the options describing which objects will be returned
    var findOptions = new FindOptions<TItem> {
        Limit = (int)limit,
        Sort = sort,
        Projection = projection
    };

    // Next, connect to the specific collection in the database
    IMongoCollection<TItem> collection = GetCollection();

    // Now, attempt to find all the items associated with the filter and find options
    IAsyncCursor<TItem> cursor = await collection.FindAsync(filter ?? FilterDefinition<TItem>.Empty, 
        findOptions, token).ConfigureAwait(false);

     // Finally, retrieve all the items and return them
     return await cursor.ToListAsync(token).ConfigureAwait(false);
}

The problem I'm having is that this breaks in the case where I want to return all the fields from the document and therefore do not provide a projection. In such cases, I get the following exception:

System.ArgumentNullException: Value cannot be null. (Parameter 'projection')
   at MongoDB.Driver.Core.Misc.Ensure.IsNotNull[T](T value, String paramName)
   at MongoDB.Driver.KnownResultTypeProjectionDefinitionAdapter`2..ctor(ProjectionDefinition`1 projection, IBsonSerializer`1 projectionSerializer)
   at MongoDB.Driver.ProjectionDefinition`2.op_Implicit(ProjectionDefinition`1 projection)

My question is: is there a pass-through projection definition or a way to programmatically map all the fields retrieved from the MongoDB document onto my return object?


Solution

  • Thanks to @Llama's suggestion, I was able to fix this. I refactored my code to look like this:

    public async Task<IEnumerable<TItem>> GetItemsAsync(uint limit, 
        FilterDefinition<TItem> filter = null,
        SortDefinition<TItem> sort = null,
        ProjectionDefinition<TItem, TItem> projection = null,
        CancellationToken token = default) {
        return await CreateFindOperation(limit, filter, sort, projection)
            .ToListAsync(token).ConfigureAwait(false);
    }
    
    // Helper function that creates a find-operation from some common elements
    private IFindFluent<TItem, TItem> CreateFindOperation(uint? limit,
        FilterDefinition<TItem> filter = null,
        SortDefinition<TItem> sort = null,
        ProjectionDefinition<TItem, TItem> projection = null) {
    
        // First, connect to the specific collection in the database
        IMongoCollection<TItem> collection = GetCollection();
    
        // Next, create an operation to find the items that are associated with the filter
        IFindFluent<TItem, TItem> operation = collection.Find(filter ?? FilterDefinition<TItem>.Empty);
    
        // Now, if the projection operation has been provided then apply it to the
        // search and then update the search
        if (projection != null) {
            operation = operation.Project(projection);
        }
    
        // If the sort operation has been provided then modify the search with the sort operation
        if (sort != null) {
            operation = operation.Sort(sort);
        }
    
        // Finally, if the limit has been included in the request then modify the operation to only
        // return the first N items associated with the limit; otherwise, return the operation as-is
        return limit.HasValue ? operation.Limit((int)limit) : operation;
    }
    

    The helper function allows me to reuse the code for other retriever-style functions as well.