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 $expand
ing 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.
Currently CosmosQuery supports the following OData operations in both a query and subquery:
$select
$filter
$orderby
$top
$skip
$count
Although this library currently supports the use of $orderby
, $top
, and $skip
within a subquery Cosmos DB does not.
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());
}
}
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.
There are four main functions you can use depending on your usecase.
Get
- Executes the query aginst the database synchronouslyGetAsync
- Executes the query against the database asynchronouslyGetQuery
- Synchronously builds theIQueryable
but does not execute it against the databaseGetQueryAsync
- Asynchronously builds theIQueryable
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);
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);