InfoCarrier.Core¶
InfoCarrier.Core is an Entity Framework Core provider for the client side of a multi-tier
application. Your client is a WPF, Blazor WebAssembly, MAUI or console app. It gets a real
DbContext with LINQ, change tracking, the identity map, navigation fix-up, lazy loading and
transactions, but no connection string and no database driver. Queries and units of work travel to
your application server, which executes them with an ordinary EF Core provider.
// A WPF, Blazor WebAssembly, MAUI or console app.
await using var context = new ShopContext(options);
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();
recent[0].Freight = 0m;
await context.SaveChangesAsync(); // one unit of work, executed on the server
That query crosses the wire as an expression tree. Your server runs it against SQL Server, PostgreSQL, SQLite, or whatever provider it uses.
-
Two packages on nuget.org, one for the client and one for the server endpoint.
-
A working pair, start to finish, in one page.
-
A browser client and a console client, one command each.
-
Every scenario that does not behave like a normal EF Core provider.
Installing
dotnet add package InfoCarrier.Core # client and server
dotnet add package InfoCarrier.Core.AspNetCore # server endpoint
Both halves need .NET 10 and EF Core 10. If you are moving an application off the earlier
3.1 line, see Upgrading from 3.1.
Why you might want this¶
The client composes the query it needs, in an API you already know, against the same DbContext
and entity classes the server uses. There is no endpoint per screen, and no DTO layer to keep in
step.
An HTTP transport ships in the package. To use gRPC, a message bus, or a direct call in the same
process, write one small class. IInfoCarrierTransport has one method.
How it fits together¶
graph LR
A["Client process<br/>DbContext,<br/>change tracker"] -->|"expression tree,<br/>change entries"| B["Transport<br/>HTTP by default"]
B --> C["Server process<br/>DbContext on SQL Server,<br/>PostgreSQL, SQLite…"]
C -->|"rows,<br/>store-generated values"| B
B --> A
The client's DbContext compiles your query and decides which part the server can run. The server
executes that part against its own model, with its own query filters and interceptors.
SaveChanges works the same way: the change tracker produces change entries, the server replays
them against a real DbContext, and store-generated values come back.
Whatever the server cannot run, the client runs over the rows that came back. That is what makes a projection with your own method in it work, and it is the cost to watch: a filter the server cannot translate is applied after the rows have crossed the wire. Querying shows which part goes where.
What works¶
Queries, including a projection split so that the server does the data access and the client runs
the rest. SaveChanges including many-to-many graphs. Explicit and lazy loading, transactions with
savepoints, ExecuteUpdate and ExecuteDelete, complex types, JSON-mapped collections, spatial
types and compiled models.
Where the server's store is relational, which is the usual case, the client knows it. The three
inheritance mappings, table and entity splitting, EF.Functions, your own HasDbFunction
mappings and AsSplitQuery all round trip, and FromSql runs where the server grants it. See
Release notes 10.1.
Lazy loading costs one round trip for every navigation you touch, a different price here than against a local database. Loading related data compares the three ways to load. In a browser client it does not work at all, for a reason that is the browser's: see Blazor WebAssembly.
The provider runs Microsoft's own EF Core specification suite, the same suite the SQL Server, SQLite and InMemory providers run. The limitations page gives the result for this release, and describes each difference you can observe in terms of the code that triggers it.
Security¶
Your server executes an expression tree that arrived over the network. Deserialization refuses anything outside a default-deny allowlist over node kinds, types and methods, before your server sees a tree at all. Authentication and authorization are not in scope and remain yours. The security page has the detail.