ServiceComposer: strongly typed view models without shared coupling
Mauro Servienti examines how strongly typed view models in ServiceComposer can reintroduce the coupling that ViewModel Composition is meant to remove. He proposes per-service interfaces for response models and per-service request models for decomposition, and discusses a DispatchProxy idea that would drop the concrete class. Read on for the trade-offs and deployment caveats, including property name collisions.
Key points
- Typed view models catch mistakes at compile time, but a shared model couples every service to one assembly.
- Handlers can call GetComposedResponseModel<TInterface>() to see only the properties their service owns.
- The concrete view model implements every service interface and can stay internal to the composition host.
- Deploying a new service interface with an old composition host can cause a runtime type load failure.
- Properties with the same name and type across interfaces silently share one value, so use a naming convention.
- Request models can be split per service because ASP.NET model binding ignores incoming properties it does not match.
ServiceComposer lets several handlers contribute to one HTTP response, and typed view models add compile-time safety. The downside is that one shared model makes every service reference the same assembly, so a Marketing handler can overwrite Sales-owned price data and schema changes ripple across teams. The proposed fix is for each handler to request an interface covering only its own properties, while the composition host supplies one concrete class implementing them all.
The coupling moves to the composition host, which must update the concrete model when a service changes its interface, and both changes need to ship together to avoid runtime type load failures. Name collisions still happen when two interfaces share a property name and type, so a naming convention helps. A DispatchProxy variant could remove the concrete class, but it offers no whole-response type for OpenAPI tools and does not work with Native AOT.
Real-world use
- A product page composed by a gateway from a Sales service (price) and a Marketing service (name, description), where each team owns its fields.
- A BFF layer that fans a POST request out to several services, each binding only the request fields it owns.
- A migration from a monolith to microservices where a shared ViewModel assembly is being dismantled.
Why it matters
If you compose responses from several services with ServiceComposer, a shared view model quietly brings back the release coupling you wanted to remove. Per-service interfaces keep the compiler's help without it.