Skip to content

Configuration Entities and Profiles

quyvu edited this page Oct 14, 2026 · 3 revisions

Entities & Profiles Configuration

Deep dive into how FxMap discovers and registers your EntityConfigureOf<T> and ProfileOf<T> classes.

How FxMap Discovers Configs & Profiles

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>();

How It Works

1

Assembly Scanning

FxMap scans the provided assemblies for all classes inheriting from EntityConfigureOf<T> and ProfileOf<T>.

2

Registration

Each discovered entity config and profile is registered in FxMap's internal registry and linked to its IDistributedKey.

3

Handler Proxy Creation

FxMap dynamically creates handler proxies for each distributed key, linking entity configs to DTO profiles during mapping operations.

Entity Config Registration

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.

Profile Registration

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>();
});

ExposedName

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");
    }
}

❌ Profile (Won't Work)

// 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

✅ Profile (Works)

// This WORKS — using exposed name alias
UseDistributedKey<UserDistributedKey>()
    .Of(x => x.UserId)
    .For(x => x.Email, "UserEmail")      // ✅ alias
    .For(x => x.Phone, "ContactPhone");  // ✅ alias

Use Cases for ExposedName

  • 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

Assembly Marker Pattern

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>();
});

For Overloads in ProfileOf

The .For() method on a profile has three overloads:

1. Default property (no expression)

Maps the property using the entity's DefaultProperty.

.For(x => x.UserName)

2. Static expression string

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")

3. Conditional expression (runtime logic)

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"))

Complete Profile Example

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"));
    }
}

Dependent Properties: Resolve a Key First

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(...).

With MapDataAsync

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  -> Provinces

Each 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.

With GraphQL (HotChocolate)

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 1

The 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.

Rules to remember

  • 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 null is skipped: the properties that depend on it are left alone.

Things to Know About Conditional Expressions

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.

Conditions run concurrently

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.

They may run more than once per request

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.

Do not use a scoped DbContext directly in a condition

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.

HotChocolate

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.

Complete Example

// 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)));

Best Practices

  • Keep entity configs close to your domain models; keep profiles close to your DTOs
  • Declare distributed keys as sealed for better performance
  • One IDistributedKey per entity — multiple profiles can share the same key
  • Use dedicated marker classes for assembly registration

Common Issues

  • Entity configs not inheriting from EntityConfigureOf<T> won't be discovered
  • Forgetting to call UseDistributedKey<TKey>() (or the string overload UseDistributedKey("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.

Clone this wiki locally