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.
- 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.
- 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.
- 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.