Repository navigation
Migration 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.
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 )andOrders:limit(2) Namewere ignored before, and throwExpressionParseExceptionnow. - 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.
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=*.csThe second search also finds :any( and :all( and ordinary C# calls. Those are fine.
| 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.
| 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.
// 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)");
}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. |
-
IndexerNodeandIExpressionNodeVisitor.VisitIndexer. A custom visitor must drop itsVisitIndexer(and since 5.1 the whole expression engine is internal); sorting is aFunctionNodeofFunctionType.OrderBynow.OrderDirectionstays. -
ExpressionHelpersandExpressionQueryableData, a helper of the indexer that nothing in FxMap used. Copy the code you need: there is no replacement. - The exceptions
DistributedMapException.CollectionFormatNotCorrected,CollectionIndexIncorrectandCollectionOrderDirectionIncorrect.
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.
- A MongoDB server older than 5.2 does not have
$sortArray, so:orderByfails there. -
:orderBytakes 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. -
:firstand:lastdo not sort. Write:orderBybefore them: EF Core rejectsLastOrDefaulton 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
OneForAllandRestForOne.
The full details of the new functions are in Filters, Sorting & Paging.
- 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.
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