Skip to content

Data Providers Entity Framework Core

quyvu edited this page Oct 13, 2026 · 2 revisions

Entity Framework Core

Answer FxMap requests straight from your DbContext: one projection query for each request, with only the values that were asked for.

Installation

dotnet add package FxMap.EntityFrameworkCore

All FxMap.* packages must use the same version.

Configuration

The owning service describes its entities with EntityConfigureOf<T>, registers them, and lists the DbContexts that hold them:

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
 
builder.Services.AddFxMap(cfg =>
    {
        cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
        cfg.AddProfilesFromAssemblyContaining<SomeProfileAssemblyMarker>();
    })
    .AddEntityFrameworkCore(c => c.AddDbContexts(typeof(AppDbContext)));

AddEntitiesFromAssemblyContaining (or AddEntityConfig<T>) must run before AddEntityFrameworkCore, and at least one DbContext must be listed: FxMap throws at startup if either is missing.

Several DbContexts

List them all. FxMap routes each entity to the DbContext that exposes it as a DbSet:

.AddEntityFrameworkCore(c => c.AddDbContexts(typeof(UsersDbContext), typeof(ReportingDbContext)));
 
// a list works too
.AddEntityFrameworkCore(c => c.AddDbContexts([typeof(UsersDbContext), typeof(ReportingDbContext)]));

How requests are executed

  • One query for each request. All the ids and all the expressions of a request are answered by one AsNoTracking query with a projection. Navigation, filters, indexers, aggregations and projections of the expression language are turned into LINQ, so EF Core writes the SQL.
  • Each query runs in its own DI scope, with its own DbContext, so requests can run in parallel. It does not see uncommitted data held by the DbContext or the transaction of the caller.
  • Ids are parsed to the id type of the entity (string, Guid, int, long and the other numeric types, nullable too), or to a strongly typed id that has a converter. Ids that cannot be parsed are ignored.
  • Ids are answered the way they were sent. A Guid in upper case, without hyphens or in braces, "007" for 7, or an id with spaces around it is found, and matched back to the object that sent it.
  • Aggregates over an empty collection: :count and :sum give 0; :min, :max and :avg give null.
  • ExposedName(x => x.Email, "UserEmail") renames a property in expressions; the original name no longer resolves.

Any database that EF Core supports works, because FxMap sends LINQ and not SQL. The provider tests of FxMap run against a real PostgreSQL 17: see Testing & performance.

Isolate expression errors

By default, one expression that the owner cannot build fails the whole request, and with it every property of the same key and the same level. It happens when an expression has an unknown function, a syntax error, or an unknown property. A rolling deployment shows it: a new consumer asks for a property that an old owner does not have yet.

Since 4.1.3 the owner can answer the expressions it can build, and report the others in the response:

builder.Services.AddFxMap(cfg => { /* ... */ })
    .AddEntityFrameworkCore(c =>
    {
        c.AddDbContexts(typeof(AppDbContext));
        c.IsolateExpressionErrors();
    });

Without IsolateExpressionErrors() nothing changes: the request fails. With it, each expression that cannot be built is reported once in ItemsResponse.Errors as an ExpressionError(Expression, Message), whatever the number of rows, and also when no row matches. The value of such an expression is null in every row.

What the service that asked does

The asking service Result
with ThrowIfException() a DistributedMapException.ExpressionsFailed that holds all the errors (Errors, and every message in Message)
without it a warning in the log for each error; the properties of these expressions are left alone and the others are mapped
with HotChocolate the fields of a broken expression are null, and ThrowIfException() throws as above

The errors travel with every transport, and a version that does not know them ignores them. MongoDB has the same setting: see the MongoDB provider.

A database error still fails the request

Only the errors raised when the expression is built are isolated. A LINQ expression that EF Core builds but cannot translate into SQL fails when the query runs, and that fails the request.

Complete example

// Domain models
public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string ProvinceId { get; set; }
    public Province Province { get; set; }
}
 
public class Province
{
    public string Id { get; set; }
    public string Name { get; set; }
    public Country Country { get; set; }
    public string CountryId { get; set; }
}
 
public class Country
{
    public string Id { get; set; }
    public string Name { get; set; }
}
 
// Distributed key and entity config (owner side)
public sealed class UserDistributedKey : IDistributedKey;
 
public class UserConfig : EntityConfigureOf<User>
{
    protected override void Configure()
    {
        Id(x => x.Id);
        DefaultProperty(x => x.Name);
        UseDistributedKey<UserDistributedKey>();
    }
}
 
// DTO and profile (consumer side)
public class UserDetailResponse
{
    public string UserId { get; set; }
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? Province { get; set; }
    public string? Country { get; set; }
}
 
public class UserDetailResponseProfile : ProfileOf<UserDetailResponse>
{
    protected override void Configure() =>
        UseDistributedKey<UserDistributedKey>()
            .Of(x => x.UserId)
            .For(x => x.Name)
            .For(x => x.Email, "Email")
            .For(x => x.Province, "Province.Name")
            .For(x => x.Country, "Province.Country.Name");
}
 
// Usage
var response = new UserDetailResponse { UserId = userId };
await distributedMapper.MapDataAsync(response);

Province.Country.Name is one navigation expression: the owner reads it with one projection query, and you do not write the join.

Batching is in the mapper

The mapper gathers the distinct ids of all the objects and sends one request for each distributed key and level, so the provider answers a hundred objects with one query, not a hundred. The N+1 article shows the difference.

Version compatibility

FxMap, FxMap.EntityFrameworkCore and the transport package must have the same version. If you wrote a data provider of your own on QueryHandlerBuilder, override BuildProjectionWithErrors to support the isolation of 4.1.3.


Source: https://fxmapper.net/docs/data-providers/ef-core/ — the website is always the most up to date version.

Clone this wiki locally