Skip to content

wbuck/CosmosQuery

Repository files navigation

CosmosQuery

CI Nuget (with prereleases)

CosmosQuery generates an Expression tree from the supplied ODataQueryOptions. This library uses AutoMapper's Queryable Extentions in conjunction with custom Expression builders to generate an Expression tree which can be parsed by the Cosmos DB LINQ provider. Furthermore, because AutoMapper is used for query projection you do not have to expose your entity types and can instead use DTO’s (Data Transfer Object) in your public facing API.


Where this library excels is how it deals with complex types. Currently OData does not provide a means of $expanding complex types. The consequence of this when using something like EFCore is that your complex data members will be null after performing a query unless the consumer of your API explicitly $select’s said data members. This can quickly become cumbersome when dealing with complex documents.


What CosmosQuery does instead is treat complex types as just another property of your entity type. In other words, all complex types are automatically expanded and pulled from the database. The data being pulled from the DB can still be controlled using the $select operator.

Supported Operations

Currently CosmosQuery supports the following OData operations in both a query and subquery:

  1. $select
  2. $filter
  3. $orderby
  4. $top
  5. $skip
  6. $count

Although this library currently supports the use of $orderby, $top, and $skip within a subquery Cosmos DB does not.

Usage

Step1


Set up your DTO to Entity mappings for AutoMapper so that it can correctly project the incoming OData query from your DTO type(s) to your Entity type(s).


If you only want to include properties explicitly (E.g., the consumer of the API has to explicitly $select which properties they want included in the results) then make sure to enable Explicit expansion on your properties.

    public class Mappings : AutoMapper.Profile
    {
        public Mappings()
        {
            CreateMap<Entity, DTO>()
                .ForMember(d => d.Name, o => o.MapFrom(s => s.FullName))                
                .ForAllMembers(o => o.ExplicitExpansion());
        }
    }

Step2


Set up the actions on your controller(s) to accept a ODataQueryOptions.
Do not decorate your actions with the [EnableQuery] attribute as this will result in some operations being applied more than once.

public class MyController : ODataController
{
    private readonly CosmosClient _client;
    private readonly IMapper _mapper;

    public MyController(CosmosClient client, IMapper mapper)
    {
        _client = client;
        _mapper = mapper;
    }

    [HttpGet]
    public async Task<IActionResult> Get(ODataQueryOptions<DTO> options)
    {
        var container = _client.GetContainer("DatabaseID", "ContainerID");
        var query = container.GetItemLinqQueryable<Entity>();
        return Ok(await query.GetQueryAsync(_mapper, options));
    }
}

And thats it, you're done!
The query and results are correctly mapped to and from your DTO and Entity type(s). This keeps your entities from being publically exposed via the API.

Functions

There are four main functions you can use depending on your usecase.

  • Get - Executes the query aginst the database synchronously
  • GetAsync - Executes the query against the database asynchronously
  • GetQuery - Synchronously builds the IQueryable but does not execute it against the database
  • GetQueryAsync - Asynchronously builds the IQueryable but does not execute it against the database

If you plan on using the synchronous versions of the functions above make sure you've enabled synchronous execution:

var container = _client.GetContainer("DatabaseID", "ContainerID");
var query = container.GetItemLinqQueryable<Entity>(allowSynchronousQueryExecution: true);

Function Signatures

public static ICollection<TModel> Get<TModel, TData>(
    this IQueryable<TData> query, 
    IMapper mapper, 
    ODataQueryOptions<TModel> options, 
    QuerySettings? querySettings = null);

public static IQueryable<TModel> GetQuery<TModel, TData>(
    this IQueryable<TData> query,
    IMapper mapper,
    ODataQueryOptions<TModel> options,
    QuerySettings? querySettings = null);

public static async Task<ICollection<TModel>> GetAsync<TModel, TData>(
    this IQueryable<TData> query, 
    IMapper mapper, 
    ODataQueryOptions<TModel> options, 
    QuerySettings? querySettings = null,
    CancellationToken cancellationToken = default);

public static async Task<IQueryable<TModel>> GetQueryAsync<TModel, TData>(
    this IQueryable<TData> query, 
    IMapper mapper, 
    ODataQueryOptions<TModel> options, 
    QuerySettings? querySettings = null,
    CancellationToken cancellationToken = default);