On this page

Check the complete request path

Follow an input through the same classes instead of testing each snippet as an unrelated program.

Expected outcomes

Request or event Observable result What must not happen
POST name Ana Rivera and email ana@example.com 201, a generated Id, IsActive false and a lookup location Client-controlled identity or activation
POST empty name and invalid email 400 with Name and Email property failures Connection factory invocation
GET existing Id 200 with the stored inactive customer Invented activation
GET unknown valid Id 404 Turn absence into an empty customer
GET empty Guid 400 Database lookup
SQL provider fails Generic 500 at the controller, original cause in application logging Relabel infrastructure failure as bad input
Request cancellation Token reaches open/query and cancellation propagates Convert cancellation into 404 or validation failure

Cancellation and client disconnect do not define a universal HTTP response status; the actual host and connection determine what can be sent. Validation messages can be localized, so compare property names and categories instead of exact text.

{ "name": "Ana Rivera", "email": "ana@example.com" }

The identifier and Location response are generated; do not hard-code their literal values in assertions.

Test without a database

A direct controller test can prove that invalid input does not reach storage. This DirectSharp test uses ordinary xUnit APIs and the same CLR application contracts; it does not need a running HTTP server or SQL Server.

import Global.System
import Global.System.Data.Common
import Global.Microsoft.AspNetCore.Mvc
import Global.Microsoft.Extensions.Logging.Abstractions
import Global.Xunit

public class CustomerControllerTests
    public action Invalid_input_does_not_open_storage()
        attributes Fact()
        let opens = 0

        action OpenConnection()
            returns DbConnection
            opens += 1
            throw new InvalidOperationException("Storage must not be reached")
        end action

        let factory as Func <of DbConnection> = action OpenConnection
        let service = new CustomerRegistrationService(
            new CustomerRepository(factory), new CustomerValidator()
        )
        let controller = new CustomersController(
            service, NullLogger <of CustomersController>.Instance
        )

        let response = await controller.Create(new CreateCustomerRequest("", "invalid"))
        let badRequest = Assert.IsType <of BadRequestObjectResult>(response)
        let problem = Assert.IsType <of ValidationProblemDetails>(badRequest.Value)
        Assert.Contains("Name", problem.Errors.Keys)
        Assert.Contains("Email", problem.Errors.Keys)
        Assert.Equal(0, opens)
    end action
end class

The await makes this ordinary parameterless action asynchronous without adding the cancellation parameter of an explicit async action. The resulting task-returning method fits the normal xUnit test contract.

Also use HTTP integration tests for routing, JSON binding, [ApiController] behavior, cancellation and authorization. Direct invocation does not exercise that pipeline. Database integration tests should separately verify inserts, missing rows and error translation against the real provider.

Make one policy change

Try the guided exercises with answers to predict naming, validation, lifetime and cancellation behavior before changing the application.

  1. Change Name.Length(2, 100) to Name.Length(3, 100). A two-character name now fails at both console and HTTP boundaries. The 100-character storage column still fits.
  2. Add MaximumLength(320) to the email rule. This aligns accepted input length with the existing SQL column. Keep EmailAddress too: length alone is not format validation.
  3. Change the maximum name length to 120. Updating only the validator is insufficient: change the SQL schema/migration, boundary tests and any downstream contract too.

The service deliberately validates again because non-HTTP callers can bypass the controller. If validation later performs asynchronous remote work, redesign that application orchestration explicitly rather than accidentally running expensive rules twice.

The reference C# host checks validation, invalid-input storage avoidance, cancellation before connection creation and the application examples. Real SQL round trips, HTTP binding/middleware and authorization require separate integration execution.

Previous: the HTTP host. Next: a concurrent customer dashboard.