Skip to content

Querying

You write EF Core LINQ. There is no InfoCarrier query API.

List<Order> recent = await context.Orders
    .Include(o => o.Customer)
    .Where(o => o.Customer!.Country == "Germany" && o.Freight > 50m)
    .OrderByDescending(o => o.PlacedOn)
    .Take(20)
    .ToListAsync();

One round trip. The Where, the OrderByDescending and the Take all execute on the server, and 20 rows come back. Nothing is filtered in the client.

What runs where

The client compiles your query, works out how much of it the server can run, and sends that much. There are three cases, and the projection usually tells you which one you are in.

The whole query goes

If the server can execute everything in the query, the entire tree is sent and the server answers with rows or a scalar. An aggregate returns a number, not the rows behind it.

var byCustomer = await context.Orders
    .GroupBy(o => o.CustomerId)
    .Select(g => new { CustomerId = g.Key, Count = g.Count(), Total = g.Sum(o => o.Freight) })
    .ToListAsync();

The query is split

If the projection contains code the server cannot run, such as one of your own methods, the query is cut at that point: the server runs the part that reaches the data, and the client runs the projection over what comes back.

public static class Formatting
{
    public static string Describe(decimal freight) => $"EUR {freight:0.00}";
}

var report = await context.Orders
    .Where(o => o.Freight > 0m)                                          // (1) server
    .Select(o => new { o.Id, Label = Formatting.Describe(o.Freight) })   // (2) client
    .ToListAsync();
  1. The filter is part of the tree the server executes, so only matching rows cross the wire.
  2. Formatting.Describe runs in your client process, over the rows that arrived.

The filter is not dragged back to the client just because the projection cannot be translated, and an unfiltered query with a client-side projection therefore fetches everything. Put the Where in before you worry about the Select. Formatting.Describe has to be a static method on a type, because a local function will not compile inside a query.

The query cannot be translated

Where EF Core itself would refuse a query, this provider refuses it too, with the same InvalidOperationException. Catch the type and never match on the message: message text is not a supported contract on any EF Core provider, and a couple of messages here are worded differently from other providers'. See Limitations.

Rules that come from the server's store

The client never sees the server's provider, so it assumes the store is relational and refuses three queries that every relational provider refuses:

Query Why
An OrderBy key of a type the wire cannot carry No store can sort by it, and answering here means sorting the whole table on the client
new Layout() ?? fallback in a predicate or an ordering The same, and the coalesce does nothing, because new never returns null
Distinct, Union, Concat, Except or Intersect over a projection that carries a collection The columns that identify a row do not survive it

If your server's store is not relational, say so once and the three go away:

optionsBuilder.UseInfoCarrier(client, o => o.UseNonRelationalServerStore());

That tells the client something it cannot work out for itself. It does not make the query work against a relational server; it only removes the refusal here.

Tracking

Change tracking works as it does with any provider, identity map and navigation fix-up included. For a read-only screen, opt out with AsNoTracking(). It is worth more here than in a local application: it skips change-tracking state for rows you are only going to display, and it keeps a long-lived client context from accumulating entities it will never write.

Paging

Compose Skip and Take before materializing:

IQueryable<Customer> query = context.Customers;

if (!string.IsNullOrEmpty(country))
{
    query = query.Where(c => c.Country == country);
}

List<Customer> page = await query
    .OrderBy(c => c.Company)          // (1)
    .Skip(pageIndex * pageSize)
    .Take(pageSize)
    .ToListAsync();

int matching = await query.CountAsync();
  1. Order before you page, and order on the entity. Sort a client-side projection instead and the server pages an unordered set while the client sorts the page: one page of the wrong rows, in the right order.

Skip and Take written after the Select run in the database too, even when the projection calls a method of your own.

Bulk operations

ExecuteUpdate and ExecuteDelete run on the server and never load the rows.

int updated = await context.Orders
    .Where(o => o.CustomerId == "AROUT")
    .ExecuteUpdateAsync(s => s.SetProperty(o => o.Freight, o => o.Freight + 1m));

int deleted = await context.Orders
    .Where(o => o.PlacedOn < cutoff)
    .ExecuteDeleteAsync();

Both return the number of rows affected and, as with any EF Core provider, neither updates your local change tracker.

What is not part of the surface

Relational-only APIs are not part of this provider: Database.ExecuteSqlRaw, GetDbTransaction, migrations and EnsureCreated. Schema management belongs on the server, where the real provider is. Expose it as a server-side operation of your own.

The model is a different matter. The client builds EF Core's relational model, so GetTableName() and Model.GetRelationalModel() answer, and tooling that reads them works against a client context. They read your own [Table] attributes and DbSet names, which both halves compile, so the two models agree. Nothing the client computes from them reaches the server.

FromSql and Database.SqlQuery<T> work, but only where the server opts in. The server calls services.AddInfoCarrierArbitrarySqlExecution() and the client o.AllowArbitrarySqlExecution(). Without both, the query is refused like any untranslatable one.

Grant it with care. One command text runs every statement in it, and an uncomposed FromSql reaches the database unchanged, so a caller who has the grant can run any SQL the database allows, with the server's own rights. The server's query filters are not in such a query.

EF.Functions.Like, EF.Constant and EF.Parameter cross the wire, and so do the functions you map yourself with HasDbFunction. A store's own family, such as SqliteDbFunctionsExtensions, is a type this package cannot name, so name it on both halves: AddInfoCarrierAllowedTypes(...) on the server, o.AllowTypes(...) on the client.

Keys of your own types

GroupBy, Join, GroupJoin and DistinctBy each take a key. When that key is a type you declared and have not named on both halves, this client cannot ask the server to group or join by it, so the server sends the rows and the grouping happens here. The answer is correct and the response is as large as the table.

// The server reads every row of Orders, and this client groups them.
orders.GroupBy(o => new PeriodKey(o.Placed.Year, o.Placed.Month))

The query succeeds either way, so only the InfoCarrierEventId.QuerySplit log event reports it, and that event names the type. See Logging. Name the type on both halves and the same query runs in the database:

services.AddInfoCarrierAllowedTypes(typeof(PeriodKey));                       // server
optionsBuilder.UseInfoCarrier(client, o => o.AllowTypes(typeof(PeriodKey)));  // client

An anonymous key such as new { o.Year, o.Month } needs no registration.

Naming a type moves the operator to the database, which is where EF Core decides whether it can translate it. A query this client answers locally can start to fail once the type is named, with the error EF Core gives for it without this provider. A named type means the query behaves as it would if you had written it against the server.

Round trips and result size

Every materialized query is a request, so a loop that queries per item makes one request per item. Compose the query instead, or fetch what you need with Include. To count what a screen actually costs, see Counting round trips.

Requests towards the server have a default size limit; answers coming back have none, because the library has no basis for capping how large an answer your own query may have. To set one, see client configuration.