Skip to content

Migration Upgrade to 5 0

quyvu edited this page Oct 15, 2026 · 3 revisions

Upgrade to 5.0

FxMap 5.0 replaces the indexer and the filter of the expression language with functions. This guide shows what to search for, what to write instead, and how to deploy.

Do you need this guide?

Yes, if a string in your profiles or entity configurations contains the indexer [...] or a filter, which is a property followed by a condition in parentheses, such as Orders(Status = 'Done'). If you only use navigation, projections, aggregations and :any(...) or :all(...), nothing changes except the update of the packages.

What Changed

Filtering, ordering and paging a collection are now the functions :where, :orderBy, :offset, :limit, :first and :last. They chain with each other in any order and with projections. The old syntax had limits that the functions do not have: one sort key, a mandatory direction, and a filter that had to come before the other functions, so Orders:orderBy(Date):where(...) could not be written.

  • The indexer Orders[0 10 asc Date] is removed.
  • The filter Orders(Status = 'Done') is removed. Use :where(...).
  • Text left after an expression is an error. Name ) and Orders:limit(2) Name were ignored before, and throw ExpressionParseException now.
  • A few public types are removed (see Removed public API).

An expression with the old syntax is rejected when it is parsed, with a message that names the function to use. The analyzer reports it at compile time as OFX001, for the expressions written as string literals in For(...), Else(...) and so on.

Step 1: Find the Old Syntax

The simplest way is to update FxMap.Analyzers to 5.0 and build: both forms are listed as errors with the position of the expression. You can also search the code:

# the indexer
grep -rnE "\[[-0-9 ]*(asc|desc) " --include=*.cs
 
# the filter: a property followed by "(" and a condition
grep -rnE "[A-Za-z]\([A-Za-z.:]+ ?(=|!=|>|<|contains|startswith|endswith)" --include=*.cs

The second search also finds :any( and :all( and ordinary C# calls. Those are fine.

Step 2: Replace the Indexer

Before 5.0 From 5.0
Orders[asc Date] Orders:orderBy(Date)
Orders[desc Date] Orders:orderBy(Date desc)
Orders[0 asc Date] Orders:orderBy(Date):first
Orders[-1 desc Date] Orders:orderBy(Date desc):last
Orders[5 asc Date] Orders:orderBy(Date):offset(5):first
Orders[0 10 asc Date] Orders:orderBy(Date):limit(10)
Orders[5 10 desc Date] Orders:orderBy(Date desc):offset(5):limit(10)
Orders(Status = 'Done')[0 asc Date] Orders:where(Status = 'Done'):orderBy(Date):first
Orders[0 asc Date].{Id, Total} Orders:orderBy(Date):first.{Id, Total}

The results are the same: the tests of the data providers ran with both spellings. Two things were different in the indexer and are not kept: a negative number other than -1 (for example [-2 ...]) also meant "the last item", and a function after [N asc Date] with a single item was an error.

Step 3: Replace the Filter

Before 5.0 From 5.0
Orders(Status = 'Done') Orders:where(Status = 'Done')
Orders(Status = 'Done'):sum(Total) Orders:where(Status = 'Done'):sum(Total)
Orders(Year = 2024, Status = 'Active').Items Orders:where(Year = 2024, Status = 'Active').Items
Company.Departments(IsActive = true).Employees:count Company.Departments:where(IsActive = true).Employees:count
Provinces(Name:count > 3).{Id, Name} Provinces:where(Name:count > 3).{Id, Name}

The condition inside is the same as before: the operators, &&, ||, the comma, and functions such as Items:count > 5. :any(condition) and :all(condition) are not changed.

One consequence: a function now belongs to the segment it follows, as every other function does. The tree of Customer.Orders:where(...):groupBy(X).{...} is Navigation[Customer, Projection(GroupBy(Where(Orders)))]. Custom visitors of FilterNode are not affected, because FilterNode stays.

Example: a Profile Before and After

// 4.x
public class UserOrdersProfile : ProfileOf<UserOrdersResponse>
{
    protected override void Configure() =>
        UseDistributedKey<UserDistributedKey>()
            .Of(x => x.UserId)
            .For(x => x.TotalSpent, "Orders(Status = 'Done'):sum(Total)")
            .For(x => x.LastOrder, "Orders[-1 asc Date]")
            .For(x => x.Top5, "Orders(Status = 'Done')[0 5 desc Total]");
}
 
// 5.0
public class UserOrdersProfile : ProfileOf<UserOrdersResponse>
{
    protected override void Configure() =>
        UseDistributedKey<UserDistributedKey>()
            .Of(x => x.UserId)
            .For(x => x.TotalSpent, "Orders:where(Status = 'Done'):sum(Total)")
            .For(x => x.LastOrder, "Orders:orderBy(Date):last")
            .For(x => x.Top5, "Orders:where(Status = 'Done'):orderBy(Total desc):limit(5)");
}

Step 4: Deploy the Services Together

An expression is sent as text to the service that owns the data, and that service parses it. An owner on 5.0 rejects [...] and Name(...), and an owner before 5.0 does not know :orderBy or :where. There is no spelling that both versions understand, so deploy the owners and the consumers that use these expressions in the same window.

Owner Consumer Result
5.0 5.0, with :where, :orderBy and the other functions Works.
before 5.0 5.0, with the new functions Fails: the owner does not know :where or :orderBy.
5.0 before 5.0, with [...] or Name(...) Fails: the owner rejects the old syntax.
any any, with no indexer and no filter Works: expressions that do not use the removed syntax are not affected.

Removed Public API

  • IndexerNode and IExpressionNodeVisitor.VisitIndexer. A custom visitor must drop its VisitIndexer (and since 5.1 the whole expression engine is internal); sorting is a FunctionNode of FunctionType.OrderBy now. OrderDirection stays.
  • ExpressionHelpers and ExpressionQueryableData, a helper of the indexer that nothing in FxMap used. Copy the code you need: there is no replacement.
  • The exceptions DistributedMapException.CollectionFormatNotCorrected, CollectionIndexIncorrect and CollectionOrderDirectionIncorrect.

Text Left After an Expression

ExpressionParser.Parse used to ignore what was left after the expression. It now throws ExpressionParseException: Unexpected '...' at position N. An expression that worked only because a part of it was ignored fails now.

This also fixes a silent bug: Orders(Status = 'Done'):orderBy(Total):first.{Id} in 4.x returned the whole order, because the projection after a function that followed a filter was dropped without an error. It is parsed now.

Limits to Know

  • A MongoDB server older than 5.2 does not have $sortArray, so :orderBy fails there.
  • :orderBy takes property names only, not functions or navigation. With MongoDB, the keys are named as written in the expression: a field renamed with [BsonElement] is not translated.
  • :first and :last do not sort. Write :orderBy before them: EF Core rejects LastOrDefault on an unordered query.
  • NATS: a request sent in the few milliseconds after a server was stopped can reach its subscription before the NATS server has processed the unsubscribe, and ends at the request timeout. It matters only when a server is stopped and started again at once, as the supervisor does with OneForAll and RestForOne.

The full details of the new functions are in Filters, Sorting & Paging.

Checklist

  • Update every FxMap.* package of every service to 5.0.0 (all on the same version).
  • Build with FxMap.Analyzers and fix each OFX001 error with the tables above.
  • Search for the old syntax in expressions that are built at run time (the analyzer sees only string literals).
  • Remove VisitIndexer from your expression visitors, and replace the removed helpers and exceptions if you used them.
  • Check that your MongoDB server is 5.2 or newer if you use :orderBy.
  • Deploy the owners and the consumers of the changed expressions in the same window.

The changelog of the release is docs/changelogs/v5.0.0.md in the repository.


Source: https://fxmapper.net/docs/migrate-to-v5/ — the website is always the most up to date version.

Clone this wiki locally