Repository navigation
Configuration Entities and Profiles
Deep dive into how FxMap discovers and registers your EntityConfigureOf<T> and ProfileOf<T> classes.
FxMap uses two separate assembly scans: one for entity configs on the data-provider side, and one for DTO profiles on the consumer side.
cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
cfg.AddProfilesFromAssemblyContaining<SomeProfileAssemblyMarker>();1
FxMap scans the provided assemblies for all classes inheriting from EntityConfigureOf<T> and ProfileOf<T>.
2
Each discovered entity config and profile is registered in FxMap's internal registry and linked to its IDistributedKey.
3
FxMap dynamically creates handler proxies for each distributed key, linking entity configs to DTO profiles during mapping operations.
Register the assembly containing your EntityConfigureOf<T> classes:
using FxMap;
builder.Services.AddFxMap(cfg =>
{
// Register assembly using any type from that assembly
cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
});Tip: You can use any type from the assembly as the marker. It's common to use a dedicated empty marker class like EntityAssemblyMarker.
Register the assembly containing your ProfileOf<T> classes separately — typically on the consumer side:
using FxMap;
builder.Services.AddFxMap(cfg =>
{
cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
cfg.AddProfilesFromAssemblyContaining<SomeProfileAssemblyMarker>();
});The ExposedName FluentAPI method allows you to expose an entity property under an alias. Consumers reference the alias in their profile expressions, hiding internal property names.
// Entity config with ExposedName
public class UserConfig : EntityConfigureOf<User>
{
protected override void Configure()
{
Id(x => x.Id);
DefaultProperty(x => x.Name);
UseDistributedKey<UserDistributedKey>();
// Client must use "UserEmail" instead of "Email"
ExposedName(x => x.Email, "UserEmail");
// Client must use "ContactPhone" instead of "PhoneNumber"
ExposedName(x => x.PhoneNumber, "ContactPhone");
}
}// This will NOT work — using actual property name
UseDistributedKey<UserDistributedKey>()
.Of(x => x.UserId)
.For(x => x.Email, "Email") // ❌ real name
.For(x => x.Phone, "PhoneNumber"); // ❌ real name// This WORKS — using exposed name alias
UseDistributedKey<UserDistributedKey>()
.Of(x => x.UserId)
.For(x => x.Email, "UserEmail") // ✅ alias
.For(x => x.Phone, "ContactPhone"); // ✅ alias- API Versioning: Change internal property names without breaking consumer profiles
- Security: Hide internal naming conventions from other services
- Consistency: Provide a stable public contract across different entities
- Refactoring: Refactor internal code without affecting consumer integrations
For cleaner code, create a marker class to represent each assembly:
// In your entity configs folder
namespace YourProject.EntityConfigs;
public sealed class EntityAssemblyMarker { }
// In your profiles folder
namespace YourProject.Profiles;
public sealed class ProfileAssemblyMarker { }
// In Program.cs
builder.Services.AddFxMap(cfg =>
{
cfg.AddEntitiesFromAssemblyContaining<EntityAssemblyMarker>();
cfg.AddProfilesFromAssemblyContaining<ProfileAssemblyMarker>();
});The .For() method on a profile has three overloads:
Maps the property using the entity's DefaultProperty.
.For(x => x.UserName)Maps using a fixed expression evaluated at runtime against the entity.
.For(x => x.UserName, "Email")
.For(x => x.TotalOrders, "Orders:count")
.For(x => x.CityName, "Address.City")Selects one of two expressions at runtime based on a condition resolved from IServiceProvider. Uses the ConditionalExpressionBuilder:
.For(x => x.UserName, c => c
.If(sp => sp.GetRequiredService<IFeatureFlag>().UseFullName)
.Expression("FirstName:concat(' ', LastName)")
.Else("Name"))
// Async condition overload:
.For(x => x.UserName, c => c
.If(async (sp, ct) => await sp.GetRequiredService<IMyService>().IsEnabledAsync(ct))
.Expression("FirstName:concat(' ', LastName)")
.Else("Name"))public class OrderResponseProfile : ProfileOf<OrderResponse>
{
protected override void Configure()
{
UseDistributedKey<UserDistributedKey>()
.Of(x => x.UserId)
// 1. Default property
.For(x => x.UserName)
// 2. Static expression
.For(x => x.UserEmail, "Email")
.For(x => x.TotalOrders, "Orders:count")
// 3. Conditional — resolves which expression to use at runtime
.For(x => x.DisplayName, c => c
.If(sp => sp.GetRequiredService<IDisplaySettings>().UseFullName)
.Expression("FirstName:concat(' ', LastName)")
.Else("Name"));
}
}A property that one rule fills can be the key of another rule. ProvinceName needs a ProvinceId, but the response only has a UserId: the province id lives on the user. Declare both rules, and FxMap resolves ProvinceId before it resolves ProvinceName. You do not order anything yourself.
public class MemberResponse
{
public string UserId { get; set; }
public string UserName { get; set; }
public string ProvinceId { get; set; } // filled by the user rule, used as a key by the province rule
public string ProvinceName { get; set; }
public string CountryId { get; set; } // filled by the province rule, used as a key by the country rule
public string CountryName { get; set; }
public List<ProvinceResponse> Provinces { get; set; }
}
public class MemberResponseProfile : ProfileOf<MemberResponse>
{
protected override void Configure()
{
UseDistributedKey<UserDistributedKey>()
.Of(x => x.UserId)
.For(x => x.UserName)
.For(x => x.ProvinceId, "ProvinceId"); // level 1: the province id of the user
UseDistributedKey<ProvinceDistributedKey>()
.Of(x => x.ProvinceId) // key produced by the rule above
.For(x => x.ProvinceName)
.For(x => x.CountryName, "Country.Name")
.For(x => x.CountryId, "CountryId"); // level 2
UseDistributedKey<CountryDistributedKey>()
.Of(x => x.CountryId) // key produced by the rule above
.For(x => x.Provinces, "Provinces:orderBy(Name)"); // level 3
}
}This is the MemberResponse of the sample services. The same pattern works for any chain: a property filled by .For(...) is a key of the next .Of(...).
Call the mapper as usual. FxMap works out the levels from the profile: what the response already holds (UserId) is level 0, and each key produced by a rule is one level deeper.
await distributedMapper.MapDataAsync(members);
// Level 1 User ids: the distinct UserId values -> UserName, ProvinceId
// Level 2 Province ids: the ProvinceId values just filled -> ProvinceName, Country.Name, CountryId
// Level 3 Country ids: the CountryId values just filled -> ProvincesEach level is one batched request per distributed key, whatever the number of objects. Levels run one after the other, because a level needs the keys the previous one produced, so a chain of three costs three sequential round trips. Keep chains as short as your model allows, and see how FxMap batches requests by key and level for a worked example.
The client does not have to select the key. If it asks only for provinceName, FxMap still resolves ProvinceId first (which needs the user), and then the province:
query {
members {
id
provinceName # provinceId is not selected
}
}
# FxMap resolves, for each batch of members:
# 1. ProvinceId from the User service (UserId is already on the member)
# 2. ProvinceName from the Province service, with the ProvinceId of step 1The key is used internally and is not part of the answer unless the query selects it. Fields of the same level, from all the members in the query, are batched together by a DataLoader, so the number of requests does not grow with the number of members. See the HotChocolate integration.
- The key of a rule (
.Of(x => x.ProvinceId)) must be filled by another rule, or already be on the object before the mapping starts. - Write the key property on the rule of the entity that owns it:
.For(x => x.ProvinceId, "ProvinceId")on the user rule. Its expression is the property of the user that holds the province id. - A response type has exactly one
ProfileOf<T>, with every rule in it: a second profile for the same type fails at startup with an "Ambiguous profile configuration" error. The same distributed key can appear in more than one group, for example two different province ids on the same response. - A key that is
nullis skipped: the properties that depend on it are left alone.
A conditional expression (.If(...).Expression(...).Else(...)) runs your code while FxMap maps a request. How and when it runs decides how you can safely write the condition.
The conditions of all the rules in a request are resolved in parallel: one task for each mapped property, and the distributed keys are fetched in parallel too. They run on the IServiceProvider the mapper was resolved from. FxMap does not serialize them, because resolving them one after another would make a mapping with many properties wait for each condition in turn.
A condition runs once for each mapped object that uses the rule. Map 100 orders and a rule on the order profile can evaluate its condition up to 100 times. Keep conditions cheap and free of side effects, and cache anything that is expensive.
Two conditions that use the same scoped instance at the same time throw InvalidOperationException ("A second operation was started on this context instance"). Create what you need inside the delegate instead, for example with IDbContextFactory:
.For(x => x.Price, c => c
.If(async (sp, ct) =>
{
var factory = sp.GetRequiredService<IDbContextFactory<AppDbContext>>();
await using var db = await factory.CreateDbContextAsync(ct);
return await db.Features.AnyAsync(f => f.Name == "NewPricing" && f.Enabled, ct);
})
.Expression("NewPrice")
.Else("Price"))A scope from IServiceScopeFactory.CreateAsyncScope() works too. Keep in mind that services resolved in that new scope do not see the state of the scope of the request.
From FxMap 4.1.1 the condition receives RequestServices, the scope of the request. Scoped services that your own middleware filled for the request (the current user, the tenant) are visible to the condition.
In exchange, the fields of a query are resolved in parallel on that one scope, so the scoped DbContext rule above applies here as well. A rule without a condition does not go through the resolution at all. In 4.1.0 the condition received the scope of the field being resolved, which is why request-scoped services were empty there.
// UserDistributedKey.cs
public sealed class UserDistributedKey : IDistributedKey;
// UserConfig.cs (data-provider side)
public class UserConfig : EntityConfigureOf<User>
{
protected override void Configure()
{
Id(x => x.Id);
DefaultProperty(x => x.Name);
UseDistributedKey<UserDistributedKey>();
ExposedName(x => x.Email, "UserEmail");
}
}
// UserResponseProfile.cs (consumer side)
public class UserResponseProfile : ProfileOf<UserResponse>
{
protected override void Configure()
{
UseDistributedKey<UserDistributedKey>()
.Of(x => x.UserId)
.For(x => x.UserName)
.For(x => x.UserEmail, "UserEmail");
}
}
// Program.cs
builder.Services.AddFxMap(cfg =>
{
cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
cfg.AddProfilesFromAssemblyContaining<SomeProfileAssemblyMarker>();
})
.AddEntityFrameworkCore(options => options.AddDbContexts(typeof(AppDbContext)));- Keep entity configs close to your domain models; keep profiles close to your DTOs
- Declare distributed keys as
sealedfor better performance - One
IDistributedKeyper entity — multiple profiles can share the same key - Use dedicated marker classes for assembly registration
- Entity configs not inheriting from
EntityConfigureOf<T>won't be discovered - Forgetting to call
UseDistributedKey<TKey>()(or the string overloadUseDistributedKey("KeyName")) in an entity config breaks the contract - Registering the wrong assembly will cause configs to not be found
Source: https://fxmapper.net/docs/configuration/entities-and-profiles/ — the website is always the most up to date version.
Introduction
Getting Started
Configuration
- Service Registration
- Entities & Profiles
- Handlers
- Pipelines
- Strongly Typed IDs
- Error Handling
- Retry Policy
- Supervisor
- Telemetry
- Kestrel (HTTP/2)
- Analyzers
Expressions
- Overview
- Property Access
- Filters, Sorting & Paging
- Functions
- Aggregations
- Projections
- Coalesce & Ternary
- GroupBy
Data Providers
Transports
Integrations
Quality
AI Assistants
Migration
Release Notes