ServiceComposer.AspNetCore

Getting started

The problem

In a system built from multiple autonomous services, each service owns its own data. Displaying a product page, for example, might require:

The simplest approach is to call multiple service APIs from the front end and merge the results there. But this couples the client to service internals, leaks service boundaries, and causes chattiness. Another common reaction is to share a database or build a dedicated aggregation service — but both approaches erode service autonomy and lead to a distributed monolith.

ViewModel Composition is a technique that solves this at the API gateway layer. Each service contributes its own slice of data through a small, independent handler. The gateway runs all handlers in parallel and returns a single merged response to the client. No service knows about the others, and the client makes a single request.

ServiceComposer is an ASP.NET Core implementation of this pattern.

Prerequisites

Installation

Add the NuGet package to the gateway project:

dotnet add package ServiceComposer.AspNetCore

Setting up the gateway

Configure ServiceComposer in Program.cs:

var builder = WebApplication.CreateBuilder();
builder.Services.AddRouting();
builder.Services.AddViewModelComposition();

var app = builder.Build();
app.MapCompositionHandlers();
app.Run();

snippet source | anchor

That is all the gateway project needs. ServiceComposer scans loaded assemblies at startup and automatically discovers composition handlers.

Writing composition handlers

Create a class library project for each service’s composition handlers (e.g. Sales.ViewModelComposition, Marketing.ViewModelComposition). Add a package reference to ServiceComposer.AspNetCore in each, then define a handler class.

A handler implements ICompositionRequestsHandler and is decorated with an [Http*] routing attribute matching the route it contributes to. Multiple handlers from different assemblies can be registered for the same route — they run in parallel and each writes its own properties onto the shared view model.

Sales handler — contributes price data:

public class SalesProductInfo : ICompositionRequestsHandler
{
    [HttpGet("/product/{id}")]
    public Task Handle(HttpRequest request)
    {
        var vm = request.GetComposedResponseModel();

        //retrieve product details from the sales database or service
        vm.ProductId = request.HttpContext.GetRouteValue("id").ToString();
        vm.ProductPrice = 100;

        return Task.CompletedTask;
    }
}

snippet source | anchor

Marketing handler — contributes name and description:

public class MarketingProductInfo: ICompositionRequestsHandler
{
    [HttpGet("/product/{id}")]
    public Task Handle(HttpRequest request)
    {
        var vm = request.GetComposedResponseModel();

        //retrieve product details from the marketing database or service
        vm.ProductName = "Sample product";
        vm.ProductDescription = "This is a sample product";
        
        return Task.CompletedTask;
    }
}

snippet source | anchor

Both handlers target the same route (/product/{id}), but they are completely independent: neither knows about the other, and they are free to evolve separately.

[!NOTE] Each service should own distinct, non-overlapping properties on the view model. Writing to the same property from two handlers running in parallel produces a data race. See thread safety for details.

Making the gateway aware of your handlers

Reference each handler class library from the gateway project. ServiceComposer’s assembly scanner picks up any ICompositionRequestsHandler implementation in a loaded assembly automatically.

<!-- In the gateway .csproj -->
<ItemGroup>
  <ProjectReference Include="..\Sales.ViewModelComposition\Sales.ViewModelComposition.csproj" />
  <ProjectReference Include="..\Marketing.ViewModelComposition\Marketing.ViewModelComposition.csproj" />
</ItemGroup>

In production, handlers are typically packaged as separate NuGet packages and referenced that way rather than via project references.

Trying it out

Run the gateway and issue a GET request to /product/1. The response is a single merged JSON object:

{
  "productId": "1",
  "productPrice": 100,
  "productName": "Sample product",
  "productDescription": "This is a sample product"
}

No client-side merging, no shared database, no knowledge between services.

How it works

When a request arrives at a composition endpoint, ServiceComposer:

  1. Resolves all handlers registered for that route
  2. Runs all Handle() methods in parallel via Task.WhenAll
  3. Each handler reads from the request and writes to the shared view model
  4. The composed view model is serialized and returned to the caller

Adding a new service means adding a new handler class library. Existing handlers require no changes.

Next steps

Topic When you need it
Strongly typed view models Replace dynamic with a concrete class
Events Coordinate between handlers within a request (e.g. composing lists)
Model binding Bind request body, route values, and query strings in handlers
Contract-less handlers Controller-action syntax without implementing an interface
Authentication and authorization Secure composition routes
Thread safety Handle shared dependencies safely
Scatter/Gather Fan out HTTP requests to downstream services