Skip to content

Configuration Handlers

quyvu edited this page Oct 13, 2026 · 2 revisions

Handler Configuration

How FxMap answers a request on the data-provider side, and the extension points that replaced custom handlers: pipeline behaviors and custom expressions.

What Is a Handler Now?

On the service that owns the data, the last step of every request is an IQueryOfHandler<TModel, TDistributedKey>. It fetches the entities for the requested ids and expressions. You do not write one for normal use: the EF Core and MongoDB data providers register it for every entity you configure.

IMappableRequestHandler no longer exists

The IMappableRequestHandler<TKey> interface from FxMap 1.x has been removed, and handlers are no longer registered by assembly scanning. To customize how requests are processed, use pipeline behaviors (below). To compute values that do not map to a model property, use custom expressions.

Pipelines: The Extension Point

Received Pipelines

Run on the data-provider side before data is fetched and returned.

✅ cfg.AddReceivedPipelines(...)

Intercept incoming mapping requests

Send Pipelines

Run on the consumer side before a mapping request is dispatched.

✅ cfg.AddSendPipelines(...)

Intercept outgoing mapping requests

Configuration Syntax

builder.Services.AddFxMap(cfg =>
{
    cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
    cfg.AddProfilesFromAssemblyContaining<SomeProfileAssemblyMarker>();
 
    // Register pipeline behaviors
    cfg.AddReceivedPipelines(c => c.OfType(typeof(LoggingReceivedPipeline<>)));
    cfg.AddSendPipelines(c => c.OfType(typeof(LoggingSendPipeline<>)));
});

v2.0 Breaking Change: AddHandlersFromNamespaceContaining has been removed. Pipelines are now the only handler registration path. Remove any calls to this method when migrating from 1.x.

The Request Flow

  1. The consuming service resolves a profile and builds one request per distributed key. Send pipelines run first.
  2. The request travels over the transport (or is answered locally when the owner is the same service).
  3. On the owning service, received pipelines run in registration order, each calling next to continue.
  4. The last step is the IQueryOfHandler of the data provider, which reads the data. Custom expressions are answered by their own behaviors and merged into the result.

An entity needs exactly one handler. If a service owns an entity but no data provider is registered, FxMap throws CannotFindHandlerForDistributedKey; if two handlers answer for the same model and key it throws DistributedKeyHasBeenConfiguredForModel.

Writing a Received Pipeline

A received pipeline wraps the handler. It can log, authorize, validate, change the request, or return early without calling next:

using FxMap.Abstractions;
using FxMap.Delegates;
using FxMap.Responses;
 
public sealed class LoggingReceivedPipeline<TKey>(ILogger<LoggingReceivedPipeline<TKey>> logger)
    : IReceivedPipelineBehavior<TKey> where TKey : IDistributedKey
{
    public async Task<ItemsResponse<DataResponse>> HandleAsync(
        RequestContext<TKey> requestContext,
        ReceivedHandlerDelegate next)
    {
        logger.LogInformation("Request for {Key}: {Count} ids",
            typeof(TKey).Name, requestContext.Query.SelectorIds.Length);
 
        return await next.Invoke();   // continue to the next pipeline, then the handler
    }
}
 
// Program.cs
cfg.AddReceivedPipelines(c => c.OfType(typeof(LoggingReceivedPipeline<>)));

RequestContext<TKey> carries Query.SelectorIds (the ids to fetch), Query.Expressions (the expressions from the profile), the headers and the cancellation token. Send pipelines work the same way with ISendPipelineBehavior<TKey>. See Pipelines for the full guide.

Custom Expressions

When a profile asks for an expression that is not a property of the entity, the owning service can answer it with an ICustomExpressionBehavior<TKey>. It returns one value for each requested id:

public sealed class GreetingExpression : ICustomExpressionBehavior<UserDistributedKey>
{
    public string CustomExpression() => "Greeting";
 
    public Task<Dictionary<string, object>> HandleAsync(RequestContext<UserDistributedKey> requestContext)
    {
        var result = requestContext.Query.SelectorIds
            .ToDictionary(id => id, object (id) => $"Hello, {id}");
        return Task.FromResult(result);
    }
}
 
// Owning service: Program.cs
cfg.AddCustomExpressionPipelines(c => c.OfType<GreetingExpression>());
 
// Consuming service: profile
UseDistributedKey<UserDistributedKey>()
    .Of(x => x.UserId)
    .For(x => x.Greeting, "Greeting");

Which Extension Point Should I Use?

You want to Use
Log, authorize, validate or cache requests on the owning service IReceivedPipelineBehavior
Add headers, trace or change requests before they leave the consuming service ISendPipelineBehavior
Return a computed value that is not a property of the entity ICustomExpressionBehavior
Read from a store that has no FxMap data provider Write a data provider (an IQueryOfHandler). This is advanced: see the data provider notes in the v4.0.0 changelog.

Best Practices

  • Prefer a pipeline over a new data provider; most needs are cross-cutting concerns.
  • Always call next unless you mean to short-circuit the request.
  • Pipelines can run for many requests at once, so keep them stateless and use dependency injection for what they need.

Source: https://fxmapper.net/docs/configuration/handlers/ — the website is always the most up to date version.

Clone this wiki locally