Project Query Results



Projections overview

What are projections:

  • A projection refers to the transformation of query results into a customized structure,
    modifying the shape of the data returned by the server.

  • Instead of retrieving the full document from the server and then picking relevant data from it on the client,
    you can request a subset of the data, specifying the document fields you want to get from the server.

  • The query can load related documents and have their data merged into the projection results.

  • Objects and arrays can be projected, fields can be renamed, and any calculations can be made within the projection.

  • Content from inner objects and arrays can be projected.
    An alias name can be given to the projected fields, and any calculations can be made within the projection.

When to use projections:

  • Projections allow you to tailor the query results specifically to your needs.
    Getting specific details to display can be useful when presenting data to users or populating user interfaces.
    Projection queries are also useful with subscriptions since all transformation work is done on the server side without having to send a lot of data over the wire.

  • Returning partial document data from the server reduces network traffic,
    as clients receive only relevant data required for a particular task, enhancing overall performance.

  • Savings can be significant if you need to show just a bit of information from a large document. For example:
    the size of the result when querying for all "Orders" documents where "Company" is "companies/65-A" is 19KB.
    Performing the same query and projecting only the "Employee" and "OrderedAt" fields results in only 5KB.

  • However, when you need to actively work with the complete set of data on the client side,
    then do use a query without a projection to retrieve the full document from the server.

Projections are not tracked by the session:

  • On the client side, the resulting projected entities returned by the query are Not tracked by the Session.

  • Any modification made to a projection entity will not modify the corresponding document on the server when SaveChanges is called.

Projections are the final stage in the query pipeline:

  • Projections are applied as the last stage in the query pipeline,
    after the query has been processed, filtered, sorted, and paged.

  • This means that the projection does Not apply to all the documents in the collection,
    but only to the documents matching the query predicate.

  • Within the projection you can only filter what data will be returned from the matching documents,
    but you cannot filter which documents will be returned. That has already been determined earlier in the query pipeline.

  • Only a single projection request can be made per Query (and DocumentQuery).
    Learn more in single projection per query.

The cost of projections:

  • Queries in RavenDB do not allow any computation to occur during the query phase.
    However, you can perform any calculations inside the projection.

  • But while calculations within a projection are allowed, having a very complex logic can impact query performance.
    So RavenDB limits the total time it will spend processing a query and its projections.
    Exceeding this time limit will fail the query. This is configurable, see the following configuration keys:

Projection Methods

Select

  • The most common way to perform a query with a projection is to use the Select method.

  • You can specify what fields from the document you want to retrieve and even provide a complex expression.

Example - Projecting individual fields of the document:

var projectedResults = session
     // Make a dynamic query on the Companies collection
    .Query<Company>()
     // Call Select to define the new structure that will be returned per Company document
    .Select(x => new
    {
        Name = x.Name,
        City = x.Address.City,
        Country = x.Address.Country
    })
    .ToList();

// Each resulting object in the list is Not a 'Company' entity,
// it is a new object containing ONLY the fields specified in the Select.
var results = await asyncSession
    // Make a dynamic query on the Companies collection
    .Query<Company>()
    // Call Select to define the new structure that will be returned per Company document
    .Select(x => new {Name = x.Name, City = x.Address.City, Country = x.Address.Country})
    .ToListAsync();

// Each resulting object in the list is Not a 'Company' entity,
// it is a new object containing ONLY the fields specified in the Select.
from "Companies"
select Name, Address.City as City, Address.Country as Country

Example - Projecting arrays and objects:

var projectedResults = session
    .Query<Order>()
    .Select(x => new
    {
        ShipTo = x.ShipTo,
        // Retrieve all product names from the Lines array in an Order document
        ProductNames = x.Lines.Select(y => y.ProductName)
    })
    .ToList();
var projectedResults = await asyncSession
    .Query<Order>()
    .Select(x => new
    {
        ShipTo = x.ShipTo,
        // Retrieve all product names from the Lines array in an Order document
        ProductNames = x.Lines.Select(y => y.ProductName)
    })
    .ToListAsync();
// Using simple expression:
from "Orders"
select ShipTo, Lines[].ProductName as ProductNames

// Using JavaScript object literal syntax:
from "Orders" as x
select {
    ShipTo: x.ShipTo, 
    ProductNames: x.Lines.map(y => y.ProductName)
}

Example - Projection with expression:

var projectedResults = session
    .Query<Employee>()
    .Select(x => new
    {
        // Any expression can be provided for the projected content
        FullName = x.FirstName + " " + x.LastName
    })
    .ToList();
var projectedResults = await asyncSession
    .Query<Employee>()
    .Select(x => new
    {
        // Any expression can be provided for the projected content
        FullName = x.FirstName + " " + x.LastName
    })
    .ToListAsync();
from "Employees" as e
select {
    FullName: e.FirstName + " " + e.LastName
}

Example - Projection with calculations:

var projectedResults = session
    .Query<Order>()
    .Select(x => new
    {
        // Any calculations can be done within a projection
        TotalProducts = x.Lines.Count,
        TotalDiscountedProducts = x.Lines.Count(x => x.Discount > 0),
        TotalPrice = x.Lines.Sum(l => l.PricePerUnit * l.Quantity)
    })
    .ToList();
var projectedResults = await asyncSession
    .Query<Order>()
    .Select(x => new
    {
        // Any calculations can be done within a projection
        TotalProducts = x.Lines.Count,
        TotalDiscountedProducts = x.Lines.Count(x => x.Discount > 0),
        TotalPrice = x.Lines.Sum(l => l.PricePerUnit * l.Quantity)
    })
    .ToListAsync();
from "Orders" as x
select {
    TotalProducts: x.Lines.length,
    TotalDiscountedProducts: x.Lines.filter(x => x.Discount > 0).length,
    TotalPrice: x.Lines
                  .map(l => l.PricePerUnit * l.Quantity)
                  .reduce((a, b) => a + b, 0)
}

Example - Projecting using functions:

// Use LINQ query syntax notation
var projectedResults = (from e in session.Query<Employee>()
    // Define a function
    let format = (Func<Employee, string>)(p => p.FirstName + " " + p.LastName)
    select new
    {
        // Call the function from the projection
        FullName = format(e)
    })
    .ToList();
// Use LINQ query syntax notation
var projectedResults = await (from e in asyncSession.Query<Employee>()
    // Define a function
    let format = (Func<Employee, string>)(p => p.FirstName + " " + p.LastName)
    select new
    {
        // Call the function from the projection
        FullName = format(e)
    })
    .ToListAsync();
declare function output(e) {
    var format = p => p.FirstName + " " + p.LastName;
    return { FullName: format(e) };
}
from "Employees" as e select output(e)

Example - Projecting using a loaded document:

// Use LINQ query syntax notation
var projectedResults = (from o in session.Query<Order>()
    // Use RavenQuery.Load to load the related Company document
    let c = RavenQuery.Load<Company>(o.Company)
    select new
    {
        CompanyName = c.Name, // info from the related Company document
        ShippedAt = o.ShippedAt // info from the Order document
    })
    .ToList();
// Use LINQ query syntax notation
var projectedResults = (from o in asyncSession.Query<Order>()
    // Use RavenQuery.Load to load the related Company document
    let c = RavenQuery.Load<Company>(o.Company)
    select new
    {
        CompanyName = c.Name, // info from the related Company document
        ShippedAt = o.ShippedAt // info from the Order document
    })
    .ToListAsync();
from "Orders" as o
load o.Company as c
select {
	CompanyName: c.Name,
	ShippedAt: o.ShippedAt
}

Example - Projection with dates:

var projectedResults = session
    .Query<Employee>()
    .Select(e => new
    {
        DayOfBirth = e.Birthday.Day,
        MonthOfBirth = e.Birthday.Month,
        Age = DateTime.Today.Year - e.Birthday.Year
    })
    .ToList();
var projectedResults = await asyncSession
    .Query<Employee>()
    .Select(e => new
    {
        DayOfBirth = e.Birthday.Day,
        MonthOfBirth = e.Birthday.Month,
        Age = DateTime.Today.Year - e.Birthday.Year
    })
    .ToListAsync();
from "Employees" as e 
select { 
    DayOfBirth: new Date(Date.parse(e.Birthday)).getDate(), 
    MonthOfBirth: new Date(Date.parse(e.Birthday)).getMonth() + 1,
    Age: new Date().getFullYear() - new Date(Date.parse(e.Birthday)).getFullYear() 
}

Example - Projection with raw JavaScript code:

var projectedResults = session.Query<Employee>()
    .Select(e => new
    {
        // Provide a JavaScript expression to the RavenQuery.Raw method
        Date = RavenQuery.Raw<DateTime>("new Date(Date.parse(e.Birthday))"),
        Name = RavenQuery.Raw(e.FirstName, "substr(0, 3)")
    })
    .ToList();
var projectedResults = await asyncSession.Query<Employee>()
    .Select(e => new
    {
        // Provide a JavaScript expression to the RavenQuery.Raw method
        Date = RavenQuery.Raw<DateTime>("new Date(Date.parse(e.Birthday))"),
        Name = RavenQuery.Raw(e.FirstName, "substr(0, 3)")
    })
    .ToListAsync();
from "Employees" as e 
select {
    Date: new Date(Date.parse(e.Birthday)), 
    Name: e.FirstName.substr(0, 3)
}

Example - Projection with metadata:

var projectedResults = session.Query<Employee>()
    .Select(e => new
    {
        Name = e.FirstName, Metadata = RavenQuery.Metadata(e) // Get the metadata
    })
    .ToList();
var projectedResults = await asyncSession.Query<Employee>()
    .Select(e => new
    {
        Name = e.FirstName, Metadata = RavenQuery.Metadata(e) // Get the metadata
    })
    .ToListAsync();
from "Employees" as e 
select {
     Name: e.FirstName, 
     Metadata: getMetadata(e)
}

ProjectInto

  • Instead of Select, you can use ProjectInto to project all public fields from a generic type.

  • The results will be projected into objects of the specified projection class.

var projectedResults = session
    .Query<Company>()
    // Call 'ProjectInto' instead of using 'Select'
    // Pass the projection class
    .ProjectInto<ContactDetails>()
    .ToList();

// Each resulting object in the list is Not a 'Company' entity,
// it is an object of type 'ContactDetails'.
var projectedResults = await asyncSession
    .Query<Company>()
    // Call 'ProjectInto' instead of using 'Select'
    // Pass the projection class
    .ProjectInto<ContactDetails>()
    .ToListAsync();

// Each resulting object in the list is Not a 'Company' entity,
// it is an object of type 'ContactDetails'.
public class ContactDetails
{
    // The projection class contains field names from the 'Company' document
    public string Name { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }
}
from "Companies"
select Name, Phone, Fax

SelectFields

The SelectFields method can only be used by a Document Query.
It has two overloads:

// 1) Select fields to project by the projection class type
IDocumentQuery<TProjection> SelectFields<TProjection>();

// 2) Select specific fields to project
IDocumentQuery<TProjection> SelectFields<TProjection>(params string[] fields);

Using projection class type:

The projection class fields are the fields that you want to project from the document class.

// Make a dynamic DocumentQuery
var projectedResults = session.Advanced
    .DocumentQuery<Company>()
    // Call 'SelectFields'
    // Pass the projection class type
    .SelectFields<ContactDetails>()
    .ToList();

// Each resulting object in the list is Not a 'Company' entity,
// it is an object of type 'ContactDetails'.
// Make a dynamic DocumentQuery
var projectedResults = await asyncSession.Advanced
    .AsyncDocumentQuery<Company>()
    // Call 'SelectFields'
    // Pass the projection class type
    .SelectFields<ContactDetails>()
    .ToListAsync();

// Each resulting object in the list is Not a 'Company' entity,
// it is an object of type 'ContactDetails'.
public class ContactDetails
{
    // The projection class contains field names from the 'Company' document
    public string Name { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }
}
from "Companies"
select Name, Phone, Fax

Using specific fields:

The fields specified are the fields that you want to project from the projection class.

// Define an array with the field names that will be projected
var projectionFields = new string[]
{
    // Fields from 'ContactDetails' class:
    "Name", "Phone"
};

// Make a dynamic DocumentQuery
var projectedResults = session.Advanced
    .DocumentQuery<Company>()
    // Call 'SelectFields'
    // Pass the projection class type & the fields to be projected from it
    .SelectFields<ContactDetails>(projectionFields)
    .ToList();

// Each resulting object in the list is Not a 'Company' entity,
// it is an object of type 'ContactDetails' containing data ONLY for the specified fields.
// Define an array with the field names that will be projected
var projectionFields = new string[]
{
    // Fields from 'ContactDetails' class:
    "Name", "Phone"
};

// Make a dynamic DocumentQuery
var projectedResults = await asyncSession.Advanced
    .AsyncDocumentQuery<Company>()
    // Call 'SelectFields'
    // Pass the projection class type & the fields to be projected from it
    .SelectFields<ContactDetails>(projectionFields)
    .ToListAsync();

// Each resulting object in the list is Not a 'Company' entity,
// it is an object of type 'ContactDetails' containing data ONLY for the specified fields.
public class ContactDetails
{
    // The projection class contains field names from the 'Company' document
    public string Name { get; set; }
    public string Phone { get; set; }
    public string Fax { get; set; }
}
from "Companies"
select Name, Phone

Single projection per query

  • As of RavenDB v6.0, only a single projection request can be made per Query (and DocumentQuery).

  • Attempting multiple projection executions in the same query will result in an exception.

    • Query:
      Multiple Select calls or a combination of ProjectInto with a Select call will result in an exception.

    • DocumentQuery:
      Multiple SelectFields calls will result in an exception.

// For example:
try
{
    var projectedResults = session
        .Query<Company>()
        // Make first projection
        .ProjectInto<ContactDetails>()
        // A second projection is not supported and will throw
        .Select(x => new {Name = x.Name})
        .ToList();
}
catch (Exception e)
{
    // The following exception will be thrown:
    // "Projection is already done. You should not project your result twice."
}