Configuring the client¶
The whole client configuration is UseInfoCarrier, plus whichever of the three replaceable objects
you want to change.
Everything else on DbContextOptionsBuilder is EF Core's and works as usual: logging,
EnableSensitiveDataLogging, ConfigureWarnings, query-tracking behaviour, proxies.
What the client is told about the server¶
A second argument carries the three things the client cannot work out for itself, because it never sees the server's provider:
optionsBuilder.UseInfoCarrier(client, o => o
.AllowTypes(typeof(SqlServerDbFunctionsExtensions))
.AllowArbitrarySqlExecution()
.UseNonRelationalServerStore());
| Call | What it says |
|---|---|
AllowTypes(...) |
These CLR types may appear in a query beyond the ones the model implies. The usual reason is the EF.Functions family your server's provider declares. It also decides where a query runs: a key of a type you have not named here keeps its GroupBy or Join on the client, so the server sends every row. See Keys of your own types. Register the same types on the server. |
AllowArbitrarySqlExecution() |
This client will send FromSql and Database.SqlQuery<T>. The server has to grant it too, and only the server's half is a boundary. See Querying. |
UseNonRelationalServerStore() |
The server's store is not a relational database, so three relational query rules do not apply, and a change to part of a document sends the whole document. On a document store the server needs its own registration as well, and that one is what keeps nested data. See Querying. |
Leave all three alone unless one of those sentences is true of your deployment.
The three objects¶
var serializer = new SystemTextJsonInfoCarrierSerializer();
using var http = new HttpClient { BaseAddress = new Uri("https://your-app-server") };
IInfoCarrierClient client = new TransportInfoCarrierClient(
new HttpInfoCarrierTransport(http, serializer),
serializer);
| Object | Interface | Replace it when |
|---|---|---|
SystemTextJsonInfoCarrierSerializer |
IInfoCarrierSerializer |
you want a different format on the wire |
HttpInfoCarrierTransport |
IInfoCarrierTransport |
you are not using HTTP, or want to decorate every request |
TransportInfoCarrierClient |
IInfoCarrierClient |
you are hosting the server in an unusual way |
All three are safe to share, and that is a contract rather than an accident of the current build.
Construct them once: one client serves every DbContext in the application, including concurrent
ones, which is why the DI example below registers it as a singleton.
The HTTP transport¶
The third argument is the route, relative to the HttpClient's BaseAddress. It defaults to
"infocarrier", which is what MapInfoCarrier() defaults to on the server. Change one and change
the other.
Everything else about the HTTP call belongs to the HttpClient: base address, timeout, headers,
handlers, retry policies. Authenticate here.
services.AddHttpClient("infocarrier", c =>
{
c.BaseAddress = new Uri(configuration["ApiBaseUrl"]!);
c.Timeout = TimeSpan.FromSeconds(30);
})
.AddHttpMessageHandler<BearerTokenHandler>();
services.AddSingleton<IInfoCarrierClient>(sp =>
{
HttpClient http = sp.GetRequiredService<IHttpClientFactory>().CreateClient("infocarrier");
var serializer = sp.GetRequiredService<IInfoCarrierSerializer>();
return new TransportInfoCarrierClient(new HttpInfoCarrierTransport(http, serializer), serializer);
});
Payload limits¶
The serializer applies a size limit to what it will deserialize. One InfoCarrierPayloadLimits
object travels with it, and each end enforces the limit that applies to it: the server caps the
request it receives, the client caps the response it receives. Only the server's side is
default-on, because that is the side reading bytes from an untrusted peer.
var serializer = new SystemTextJsonInfoCarrierSerializer(
new InfoCarrierPayloadLimits(
maxRequestBytes: 4 * 1024 * 1024, // towards the server
maxResponseBytes: 16 * 1024 * 1024)); // back to the client
| Default | Why | |
|---|---|---|
MaxRequestBytes |
64 MiB (InfoCarrierPayloadLimits.DefaultMaxRequestBytes) |
An unauthenticated peer making your server allocate is the threat. No legitimate query tree comes near this. |
MaxResponseBytes |
null, no limit |
You asked for the result, and the library has no basis for capping how large an answer your own query may have. Set it if a runaway query should fail loudly rather than exhaust memory, or if the hop back is not one you trust. |
Pass null to opt out of a limit. It is spelled as an explicit null rather than a very large
number so that opting out is visible in your code.
MaxRequestBytes set here caps nothing on the client, which never deserializes a request. It
matters on the server, and the server builds its own serializer with its own limits. See
Configuring the server.
Synchronous calls¶
A synchronous DbContext call blocks on the async path, so the calling thread waits out the round
trip. Every await inside the provider uses ConfigureAwait(false), so it does not deadlock on a UI
synchronization context, but a WPF or WinForms caller on the UI thread freezes the window until the
answer arrives. Use the async API from a UI thread.
Logging¶
Turn on standard EF Core logging while you are learning what crosses the wire:
At Information this provider adds one event of its own, InfoCarrierEventId.QuerySplit, raised
when part of a query stays on the client. It names how many queries the server ran, which
operators stayed here, and which unregistered key types kept them here. A Where, a Skip or a
Take among them means the server sent more rows than your query asked for.
To see the payloads themselves, decorate the transport. That is what the sample's wire inspector is. See Custom transports.
Counting round trips¶
The cost of this provider is round trips. To count them in a running application, listen to the
meter named InfoCarrier.Core:
It publishes two instruments. infocarrier.client.round_trips counts every request that finished,
successful or failed. infocarrier.client.round_trip.duration records how long each one took, in
seconds. Both carry the operation as a tag, so you can tell a query from a save, and a request that
failed also carries error.type.
Any OpenTelemetry exporter reads the same meter with AddMeter("InfoCarrier.Core"). Nothing is
measured while nothing is listening.
The internal service provider¶
Build EF's internal service provider yourself for two things: to register a value mapper on the client, and to replace a provider service.
ServiceProvider providerServices = new ServiceCollection()
.AddEntityFrameworkInfoCarrier()
.AddSingleton<IInfoCarrierValueMapper, MoneyValueMapper>()
.BuildServiceProvider();
DbContextOptions options = new DbContextOptionsBuilder<ShopContext>()
.UseInternalServiceProvider(providerServices)
.UseInfoCarrier(client)
.Options;
AddEntityFrameworkInfoCarrier() registers everything the provider needs, including the value
mappers that ship with it. Build the provider once and share it: EF caches services on it, and a new
one per context is a leak. If you need neither feature, do not do this. The default path builds and
caches the service provider for you.
Client-side query filters and interceptors¶
They work, and they run on the client, so a filter defined only on the client is a convenience.
The server's own filters decide what comes back, and they are a default rather than a boundary,
because IgnoreQueryFilters() travels in the expression tree and the server honours it. See
Where the checks go and Multi-tenancy.