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();
- The filter is part of the tree the server executes, so only matching rows cross the wire.
Formatting.Describeruns 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:
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();
- 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.