Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ServiceComposer/ServiceComposer.AspNetCore/llms.txt

Use this file to discover all available pages before exploring further.

Composition handlers are the building blocks of ServiceComposer. Each handler is a small, autonomous class that handles one slice of data for a given HTTP route — for example, the Sales service contributes pricing while the Marketing service contributes product descriptions. ServiceComposer discovers all matching handlers at startup and runs them in parallel, then merges their outputs into a single JSON response. No handler knows anything about the others.

The ICompositionRequestsHandler interface

Every composition handler implements ICompositionRequestsHandler, which defines a single method:
public interface ICompositionRequestsHandler
{
    Task Handle(HttpRequest request);
}
The HttpRequest parameter gives the handler access to the full incoming request — body, route values, query string, headers, and the HttpContext. Each handler writes its own data slice onto the shared dynamic view model retrieved via request.GetComposedResponseModel().

Routing handlers to URLs

Handlers are decorated with standard ASP.NET routing attributes ([HttpGet], [HttpPost], [HttpPut], [HttpDelete], [HttpPatch]) to declare which route they respond to. Multiple handlers across different assemblies can target the exact same route template — they all run in parallel for every matching request.
1

Create a handler class library

Add a class library project per service (e.g. Sales.ViewModelComposition) and add a package reference to ServiceComposer.AspNetCore.
2

Implement ICompositionRequestsHandler

Create a class that implements the interface and decorate it with an [Http*] attribute matching the route it handles.
3

Reference the library from the gateway

Add a project reference (or NuGet reference in production) from the gateway project so that the assembly is loaded at startup.

Gateway setup

Configure ServiceComposer once in the gateway’s Program.cs:
var builder = WebApplication.CreateBuilder();
builder.Services.AddRouting();
builder.Services.AddViewModelComposition();

var app = builder.Build();
app.MapCompositionHandlers();
app.Run();
AddViewModelComposition() triggers assembly scanning. MapCompositionHandlers() registers a composition endpoint for every route covered by at least one handler.

Writing handlers that share a route

The following two handlers both target GET /product/{id}. ServiceComposer calls them concurrently via Task.WhenAll and each writes its own properties to the shared view model. Sales handler — contributes price and availability:
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;
    }
}
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;
    }
}
Both handlers are independent — they live in different assemblies and neither references the other. A GET /product/1 returns:
{
  "productId": "1",
  "productPrice": 100,
  "productName": "Sample product",
  "productDescription": "This is a sample product"
}

Assembly scanning

ServiceComposer automatically scans all loaded assemblies at startup. Any class that implements ICompositionRequestsHandler is registered as a transient component in the DI container and mapped to its declared route(s). No explicit registration is required — referencing the assembly is enough. In production, handler libraries are typically distributed as NuGet packages and referenced from the gateway project:
<!-- In the gateway .csproj -->
<ItemGroup>
  <ProjectReference Include="..\Sales.ViewModelComposition\Sales.ViewModelComposition.csproj" />
  <ProjectReference Include="..\Marketing.ViewModelComposition\Marketing.ViewModelComposition.csproj" />
</ItemGroup>

Contract-less composition handlers

Available since v4.2.0-beta.1 Contract-less handlers let you write composition logic using familiar controller-action syntax, without implementing ICompositionRequestsHandler on every class. Decorate the class with [CompositionHandler] and its methods with [Http*] attributes:
[CompositionHandler]
class SampleCompositionHandler
{
    [HttpGet("/sample/{id}")]
    public Task SampleMethod(int id, [FromQuery(Name = "c")]string aValue, [FromBody]ComplexType ct)
    {
        return Task.CompletedTask;
    }
}
This is useful when a single logical service context needs multiple handlers for different routes — instead of creating one class per route you can group them all inside a single [CompositionHandler] class. A C# source generator reads each [Http*]-decorated method at compile time and emits a concrete ICompositionRequestsHandler wrapper class. Services can be injected via [FromServices]:
[CompositionHandler]
class SampleCompositionHandlerWithServices
{
    [HttpGet("/sample/{id}")]
    public Task SampleMethod(int id, [FromServices] IMyService myService)
    {
        return Task.CompletedTask;
    }
}
Contract-less handler methods must be public or internal, return Task, and be decorated with exactly one [Http*] attribute. Supporting multiple [Http*] attributes on the same method is a known limitation.

Rules for contract-less handler methods

Access modifier

Must be public or internal.

Return type

Must return Task.

Routing

Must carry exactly one [Http*] attribute.

DI injection

Parameters marked [FromServices] are resolved from the DI container at request time.

Thread safety

Handlers run in parallel. Each service must own a distinct, non-overlapping set of properties on the view model. Writing to the same property from two concurrent handlers produces a data race and unpredictable results.
Because GetComposedResponseModel() returns a dynamic ExpandoObject-backed instance, adding two different properties concurrently is safe — but setting the same property from two handlers is not. Design your handler boundaries so that no two handlers ever write to the same view model property.

Build docs developers (and LLMs) love