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.

By default, ServiceComposer uses a C# dynamic object instance as the shared view model. Handlers call request.GetComposedResponseModel() and assign properties directly to it, without compile-time type checking. This is convenient for getting started, but as your composition layer grows you may want a strongly typed view model that provides IDE autocompletion, refactoring support, and static analysis. ViewModel factories let you replace the dynamic object with any POCO class you choose.

The default dynamic view model

Without a factory, GetComposedResponseModel() returns a dynamic backed by an ExpandoObject. Any property you assign to it is serialized into the JSON response:
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;
    }
}

Defining a strongly typed view model

View models are plain POCO classes. The only requirement is that they are serializable:
public class ProductViewModel
{
    public string ProductId { get; set; }
    public decimal ProductPrice { get; set; }
    public string ProductName { get; set; }
    public string ProductDescription { get; set; }
}

Endpoint-scoped view model factories

An IEndpointScopedViewModelFactory creates the view model for a specific route. Decorate the factory class with the same [Http*] routing attribute used by your handlers:
class ProductViewModelFactory : IEndpointScopedViewModelFactory
{
    [HttpGet("/product/{id}")]
    public object CreateViewModel(HttpContext httpContext, ICompositionContext compositionContext)
    {
        var productId = httpContext.GetRouteValue("id").ToString();
        return new ProductViewModel()
        {
            ProductId = productId
        };
    }
}
ServiceComposer’s assembly scanner discovers IEndpointScopedViewModelFactory implementations automatically and registers them in DI at startup — no manual registration is required. Each route that needs a strongly typed view model needs its own factory.
The factory receives the HttpContext and the ICompositionContext, so it can pre-populate properties from route values, as shown above with ProductId. Handlers then only need to fill in the properties they own.

Using the typed view model in handlers

Once a factory is in place, switch handlers to the generic overload GetComposedResponseModel<T>():
public class SalesProductInfo : ICompositionRequestsHandler
{
    [HttpGet("/product/{id}")]
    public Task Handle(HttpRequest request)
    {
        var vm = request.GetComposedResponseModel<ProductViewModel>();

        //retrieve product details from the sales database or service
        vm.ProductPrice = 100;

        return Task.CompletedTask;
    }
}

Global view model factories

A global factory (IViewModelFactory) is used for every route that does not have a matching endpoint-scoped factory. It is useful when all routes share a common base view model, or when a single factory can determine the correct type from the request:
public class DefaultViewModelFactory : IViewModelFactory
{
    public object CreateViewModel(HttpContext httpContext, ICompositionContext compositionContext)
    {
        // Return a shared base type, or inspect the route to determine the right type
        return new ExpandoObject();
    }
}
Like endpoint-scoped factories, global factories are discovered by assembly scanning and registered in DI automatically. Only one global IViewModelFactory per application is supported — attempting to register a second one throws a NotSupportedException.

Manual factory registration

Assembly scanning handles registration automatically, but you can also register factories explicitly through ViewModelCompositionOptions when you need more control (for example, when disabling the scanner or configuring from tests):
builder.Services.AddViewModelComposition(options =>
{
    options.RegisterEndpointScopedViewModelFactory<ProductViewModelFactory>();
    options.RegisterGlobalViewModelFactory<DefaultViewModelFactory>();
});
RegisterEndpointScopedViewModelFactory<T>() accepts any T that implements IEndpointScopedViewModelFactory. RegisterGlobalViewModelFactory<T>() accepts any T that implements IViewModelFactory — but not IEndpointScopedViewModelFactory; endpoint-scoped factories must always be registered through their own method.

Interface contracts

public interface IViewModelFactory
{
    object CreateViewModel(HttpContext httpContext, ICompositionContext compositionContext);
}

public interface IEndpointScopedViewModelFactory : IViewModelFactory
{
    // Inherits CreateViewModel; also requires an [Http*] routing attribute on the class
}

Resolution order

ServiceComposer resolves the view model factory for each request using the following priority:
1

Endpoint-scoped factory

If an IEndpointScopedViewModelFactory is registered whose route attribute matches the current request, it is used to create the view model.
2

Global factory

If no endpoint-scoped factory matches and an IViewModelFactory is registered, it is used as the fallback for all unmatched routes.
3

Built-in dynamic factory

If neither type of factory is registered, ServiceComposer creates a dynamic (ExpandoObject) view model instance — the same default behavior as when no factory is configured.
Start with the default dynamic view model while prototyping. Add an IEndpointScopedViewModelFactory for each route once you are ready to enforce type safety, and optionally add a global IViewModelFactory to cover any remaining routes with a shared base type.

Build docs developers (and LLMs) love