Your Order service stores a UserId. The API that returns orders needs the user's name and email, and those live in the User service. So every team ends up writing the same glue: loop over the orders, call the User service, add a cache, add retries, and hope it doesn't become N+1 calls across the network.
This post shows how to do that mapping over gRPC in .NET with FxMap, an open-source library (Apache-2.0) where you declare where a property comes from and one call fills it.
The usual way
foreach (var order in orders)
{
var user = await _userClient.GetUserAsync(new GetUserRequest { Id = order.UserId });
order.UserName = user.Name;
order.UserEmail = user.Email;
}
A page of 50 orders is 50 gRPC calls. You fix it with a batch endpoint, then a cache, then you write the same thing again for ProductName, ProvinceName, and every other key in every other service.
Declaring the mapping instead
Both services share a key name. It is just a marker:
public sealed class UserDistributedKey : IDistributedKey;
The User service (owner of the data) says how to find a user and what it can return:
public class UserConfig : EntityConfigureOf<User>
{
protected override void Configure()
{
Id(x => x.Id);
DefaultProperty(x => x.Name);
UseDistributedKey<UserDistributedKey>();
}
}
builder.Services.AddGrpc();
builder.Services.AddFxMap(cfg => cfg.AddEntitiesFromAssemblyContaining<UserConfig>())
.AddEntityFrameworkCore(o => o.AddDbContexts(typeof(UserDbContext)));
var app = builder.Build();
app.MapFxMapperGrpc();
app.Run();
The Order service (needs the data) describes its DTO:
public class OrderResponse
{
public string UserId { get; set; }
public string UserName { get; set; }
public string UserEmail { get; set; }
}
public class OrderResponseProfile : ProfileOf<OrderResponse>
{
protected override void Configure() =>
UseDistributedKey<UserDistributedKey>()
.Of(x => x.UserId)
.For(x => x.UserName) // the default property (Name)
.For(x => x.UserEmail, "Email");
}
builder.Services.AddFxMap(cfg =>
{
cfg.AddProfilesFromAssemblyContaining<OrderResponseProfile>();
cfg.AddGrpcClients(c => c.AddGrpcHosts("http://user-service:5001"));
});
And in the endpoint:
var orders = await db.Orders.Select(o => new OrderResponse { UserId = o.UserId }).ToListAsync();
await mapper.MapDataAsync(orders);
What happens on the wire
- FxMap collects the distinct
UserIds of all 50 orders and sends one gRPC request to the User service. - The User service answers from its
DbContextin one query and FxMap writesUserNameandUserEmailback. - Chains resolve level by level:
UserId → ProvinceId → CountryIdis one request per level, not per object. - The services share only a key name. No generated client per field, no shared DTO assembly.
You are not limited to plain properties. The expression language reads navigations, filters and aggregates on the owner's side:
.For(x => x.CountryName, "Country.Name")
.For(x => x.DoneTotal, "Orders:where(Status = 'Done'):sum(Total)")
Not only gRPC, not only .NET
gRPC is one of six transports. NATS, RabbitMQ, Kafka, Azure Service Bus and Amazon SQS use the same profiles, so switching transport doesn't touch your mapping code.
The wire format is the same in jfxmap (Java, Spring Boot) and nfxmap (Node.js, TypeScript), so a .NET Order service can fill its DTO from a Java User service.
Is it fast?
The README has a BenchmarkDotNet comparison against AutoMapper ProjectTo with the network removed: past a few dozen objects FxMap is on par or faster and allocates a half to a third of the memory, because each distinct key is fetched once. Numbers and the command to re-run them are here.
Try it
dotnet add package FxMap
dotnet add package FxMap.Grpc
dotnet add package FxMap.EntityFrameworkCore
- GitHub: https://github.com/quyvu01/FxMap
- Docs: https://fxmapper.net
- gRPC transport: https://github.com/quyvu01/FxMap/tree/main/src/FxMap.Grpc
I'm the author and I'd like to hear how you solve this today. If you have a microservice setup where this would or wouldn't fit, tell me in the comments.

Top comments (0)