The caller sends an HTTP request, but the ordering rules operate on a typed OrderPlacementRequest. Something must read the body, reject invalid wire shapes, convert valid input, call the domain, and compose the HTTP response. Exceptions also need the correct caller-safe translation.

About the examples: Meridian Ordering is a private training reference project. The inline listings illustrate the techniques; the full C# and Python repositories are not currently public.

Those responsibilities belong to the service interface. Meridian verifies them alongside the maintained domain acceptance suite. Each category has an explicit boundary and assertions for the behavior it owns.

Assign each obligation to its owning boundary

Interface testing responsibility map Controller Hook tests verify parsing and wire composition with an arranged domain response. Translator and middleware class tests verify their failure policies. Hosted HTTP tests verify routing and wiring over the real domain. Facade acceptance scenarios verify business effects. Each category owns its comparisonsController HookWire parsing andconversionDomain arrangedTranslator andmiddlewareFailure mappingContext; loggingHosted HTTPpipelineRoutes and wiringReal domainunderneathDomain facadeacceptanceBusiness outcomesStore and outboundeffects
Keep the boundary explicit in each category. Controller conversion, exception translation, hosted wiring, and domain outcomes contribute distinct evidence to the maintained suite.
Category Starts and ends at What it verifies
Facade acceptance Public domain operation and its effects Business outcomes, persistence, fulfillment, confirmation, failure state
Controller Hook Action, with the domain Hook arranged Raw-body conversion, parsing refusal, typed request, composed response, exception transparency
Translator class Exception and HTTP context Classification, status, headers, reason, content type, safe body
Middleware class Request delegate and middleware output Handling, contextual enrichment, logging, translation
Hosted pipeline HTTP request and HTTP response Route, resource shape, and middleware wiring with the real domain beneath

The categories form one maintained verification discipline with clear ownership. A controller comparison does not substitute for order-state assertions. A domain scenario cannot represent a fractional JSON quantity in an integer parameter; the wire parser owns that refusal.

Follow the production controller

Here is the complete action:

/// Act implementation: read, parse, invoke the domain Hook, and compose the response.
public async Task<IActionResult> PlaceOrder()
{
    using var requestBodyReader = new StreamReader(Request.Body);
    string requestBodyJson = await requestBodyReader.ReadToEndAsync();
    OrderPlacementRequest orderPlacementRequest = ParserOrderPlacementRequest.Parse(requestBodyJson);
    OrderPlacementResult orderPlacementResult = await PlaceOrderCoreAsync(orderPlacementRequest);
    OrderPlacementResultResource orderPlacementResultResource = ComposerOrderPlacementResponse.ComposeResult(orderPlacementResult);
    return Ok(orderPlacementResultResource);
}

The controller’s domain call is behind a protected virtual Hook. The production Hook delegates directly to the facade. A test subclass overrides it to capture the typed request and return an arranged domain outcome. That lets the controller test exercise its actual parsing and composition without creating a database scenario for every wire-shape variation.

The complete Hook subclass records the request and supports a scripted result or exception. Its parsing-only constructor fails if the Hook is reached at all, because those scenarios expect refusal before domain invocation.

Compare the request and response together

The valid-body scenario supplies raw JSON with mixed-case SKU text. It expects that text to reach the domain unchanged. Canonicalization belongs to the manager’s front door. It also supplies an arranged domain total and checks the composed wire representation of that result.

Here is the complete test:

/// Arrange, Act, Assert: verify the controller's forward and backward conversions.
public async Task PlaceOrder_WhenBodyCarriesAValidOrder_ThenTheHookReceivesTheTypedRequestAndTheResponseCarriesTheWireShapedResult()
{
    /// Arrange - the raw wire body with snake_case fields. No catalog participates
    // (the hook is scripted), so the SKU is any well-formed value - carried in
    // MIXED casing, because the controller must hand it to the domain VERBATIM:
    // canonicalization is the Manager's front-door step (TR-10), not the boundary's.
    string wireSku = LetterCasing.SwapCase(DataGenerators.CreateRandomNonexistentSku());
    int orderedQuantity = 7;
    string requestBodyJson = ApiTestHelpers.CreatePlacementRequestBodyJson(
        orderReference: _expectedOrderPlacementIdentifiers.OrderReference,
        customerId: _expectedOrderPlacementIdentifiers.CustomerId,
        sku: wireSku,
        quantity: orderedQuantity);
    // The typed request the controller must hand to the hook, and the scripted
    // domain outcome the controller must compose into the wire response.
    var expectedOrderPlacementRequest = new OrderPlacementRequest(
        orderReference: _expectedOrderPlacementIdentifiers.OrderReference,
        customerId: _expectedOrderPlacementIdentifiers.CustomerId,
        orderPlacementLineItems: [new OrderPlacementLineItem(sku: wireSku, quantity: orderedQuantity)]);
    var scriptedOrderPlacementResult = new OrderPlacementResult(
        OrderReference: _expectedOrderPlacementIdentifiers.OrderReference,
        OrderTotal: 458.97m,
        OrderPlacementStatus: OrderPlacementStatus.Placed);
    var expectedOrderPlacementResultResource = new OrderPlacementResultResource(
        OrderReference: _expectedOrderPlacementIdentifiers.OrderReference,
        OrderTotal: "458.97",
        OrderPlacementStatus: "Placed");
    var ordersController = new OrdersControllerForTest(scriptedOrderPlacementResult);
    ApiTestHelpers.AttachHttpRequestWithBody(ordersController, requestBodyJson);

    // Act
    IActionResult actualActionResult = await ordersController.PlaceOrder();

    /// Assert - request and response discrepancies are reported together.
    OrderPlacementRequest? actualOrderPlacementRequest = ordersController.CapturedOrderPlacementRequest;
    AsserterController.AssertOrderRequestAndResponseAreAsExpected(
        expectedOrderPlacementRequest, expectedOrderPlacementResultResource, actualOrderPlacementRequest, actualActionResult);
}

The arranged amount is a composition input; this category does not calculate catalog prices. The aggregate controller asserter compares the captured request and returned response together. Its complete implementation shows which fields it compares.

Parsing scenarios separately verify malformed JSON, missing fields, wrong root shape, wrong line-item shape, and non-integer quantities. They compare the required problem information and verify that the Hook was never reached.

Preserve an exception until its owning handler processes it

The controller should pass a domain exception through unchanged. Testing only ReferenceEquals would miss mutation of that same object. The test snapshots its context before Act and checks both identity and context afterward:

/// Arrange, Act, Assert: preserve exception identity and its pre-operation context.
public async Task PlaceOrder_WhenTheDomainHookThrows_ThenTheVerySameExceptionInstancePassesThroughUntouched()
{
    // domain rejection. The controller must not catch, wrap, re-create, or enrich
    // it: translation is the middleware's job. The complete contextual data is
    // snapshotted BEFORE the act, so the assertion can prove the instance came
    // through not just reference-same but unmodified.
    string requestBodyJson = ApiTestHelpers.CreatePlacementRequestBodyJson(
        orderReference: _expectedOrderPlacementIdentifiers.OrderReference,
        customerId: _expectedOrderPlacementIdentifiers.CustomerId,
        sku: DataGenerators.CreateRandomNonexistentSku(),
        quantity: 1);
    var scriptedDomainException = new CustomerNotFoundException(
        $"Order placement request was rejected: customer '{_expectedOrderPlacementIdentifiers.CustomerId}' does not exist");
    Dictionary<string, object> expectedContextualDataSnapshot =
        scriptedDomainException.ContextualDataByName.ToDictionary(
            contextualEntry => contextualEntry.Key, contextualEntry => contextualEntry.Value);
    var ordersController = new OrdersControllerForTest(scriptedDomainException);
    ApiTestHelpers.AttachHttpRequestWithBody(ordersController, requestBodyJson);

    // Act
    var actualCustomerNotFoundException = await Assert.ThrowsAsync<CustomerNotFoundException>(
        async () => await ordersController.PlaceOrder());

    /// Assert - the very same instance, untouched
    AsserterUntouchedExceptionPassThrough.AssertExceptionPassedThroughUntouched(
        expectedSameExceptionInstance: scriptedDomainException,
        expectedContextualDataSnapshot: expectedContextualDataSnapshot,
        actualException: actualCustomerNotFoundException);
}

The pass-through asserter checks removed, added, and changed entries as well as reference identity. Middleware owns later enrichment and translation. This test ensures the controller does not silently acquire that responsibility.

Verify translation and hosted wiring at their own boundaries

Translator tests construct specific exceptions and inspect the exact response status, headers, content type, and body. Business refusal messages are intended for callers; infrastructure failures need safe responses that preserve useful classification without exposing internal diagnostic context.

The complete translator tests and middleware tests verify those responsibilities independently. The hosted smoke test then verifies that the actual HTTP pipeline reaches translation.

Here is the complete host factory:

/// Arrange: host the pipeline with the real facade built through test composition.
private static WebApplicationFactory<Program> CreateApiFactory()
{
    // The whole production pipeline hosts as-is; only the facade registration is
    // replaced with one built through ServiceLocatorTesting - the same seam every
    // suite uses, moved to the composition root.
    return new WebApplicationFactory<Program>().WithWebHostBuilder(webHostBuilder =>
        webHostBuilder.ConfigureServices(services =>
        {
            services.RemoveAll<DomainFacade>();
            services.AddSingleton(_ => DomainFacadeFactory.CreateDomainFacade().DomainFacade);
        }));
}

The complete pipeline scenarios post genuine HTTP requests, use the real domain beneath, and assert route and response shape or translation presence. They do not repeat every order-state comparison. Their facade registration is replaced with test composition, so the run verifies this hosted graph rather than certifying every production registration or deployed setting.

Retain one coherent release argument

The primary behavioral artifact is the assembled domain suite with correct requirements, scenarios, expectations, and complete outcome comparisons. Interface obligations still need evidence. Meridian places that evidence in explicit categories rather than duplicating every business scenario at every layer.

The PWI Service-Boundary Testing chapter explains these ownership boundaries. Together they let a reviewer say which evidence verifies the business behavior, wire conversion, translation, and hosted registration. The purpose remains confidence to go to production with the required behavior verified before exposure.


Previous article | Series contents | Next article