Course outline

ASP.NET Core / Advanced: Verification and Reliable Operations / Lesson 18

API Evolution and Application Boundaries

Preserve public input/output, error, authorization, pagination, and lifetime contracts while changing internal implementation.

About 4 min readContent reviewed
On this page

Lesson goals

  • Review compatibility beyond method signatures.
  • Design stable DTO serialization and bounded pagination.
  • Keep architecture decisions and validation evidence reviewable.

HTTP compatibility includes behavior and data meaning

A stable API defines methods, paths, status codes, field names/types, null/presence meaning, ordering, error categories, authorization, idempotency, and cancellation effects. A handler can compile unchanged while a serializer option or policy change breaks clients. Keep request/response DTOs distinct from persistence entities when their lifecycles differ.

Versioning strategy can use a path, header, media type, or another reviewed convention, but clients need a migration/deprecation contract. Adding a required input or changing a numeric/enum representation is not automatically compatible. Do not promise that unknown-field tolerance makes every future server change safe.

Test the intended public representation explicitly

The first complete program serializes a synthetic response DTO with JsonSerializerDefaults.Web. It verifies the resulting camel-case field names and values locally. It does not prove the application selected those exact JSON options, negotiated content type, or returned the DTO through its endpoint. Those require the configured HTTP path.

Changing naming policy, enum converters, number handling, date formats, or required/null behavior can affect clients. Keep fixed protocol examples and test both expected fields and intentionally absent sensitive data. A domain object may contain internal fields that should never become public just because default serialization finds them.

csharp Check a synthetic web-default DTO representation
using System;
using System.Text.Json;

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
string json = JsonSerializer.Serialize(new ReportResponse(7, "ready"), options);
if (json != "{\"id\":7,\"state\":\"ready\"}") throw new InvalidOperationException("representation changed");
Console.WriteLine(json);

internal sealed record ReportResponse(int Id, string State);
Expected output
{"id":7,"state":"ready"}

The local serializer emits id/state; this does not execute an ASP.NET response.

Pagination needs stable ordering and a bounded continuation

The second program uses unique integer IDs, a cursor, a page size of two, and one-item lookahead to determine whether more data exists. It is a local sequence example, not an EF query-performance or transaction-consistency test. In a real dataset, nonunique sort keys need a tie-breaker and cursor fields that preserve the complete order.

Offset and keyset pagination answer different operational needs and have different change/concurrency behavior. Bound page sizes, validate cursor shape, scope data by authorization, and define snapshot/live semantics. A cursor is not an authorization token merely because it is opaque. Avoid exposing internal secrets through a serialized cursor.

csharp Check bounded local cursor pagination
using System;
using System.Linq;

int after = 2, pageSize = 2;
int[] candidates = Enumerable.Range(1, 6).Where(id => id > after).OrderBy(id => id).Take(pageSize + 1).ToArray();
int[] page = candidates.Take(pageSize).ToArray();
bool hasMore = candidates.Length > pageSize;
int? next = hasMore ? page[^1] : null;
Console.WriteLine($"ids={string.Join(",", page)} hasMore={hasMore} next={next}");
Expected output
ids=3,4 hasMore=True next=4

The page is 3,4 and the lookahead finds more data, yielding next cursor 4.

Keep HTTP, application, domain, and infrastructure responsibilities explicit

Endpoints/controllers translate HTTP, application operations coordinate use cases, domain rules protect invariants, and infrastructure implements provider/network/storage behavior. The exact structure should fit the application rather than copy a fashionable directory layout. Narrow dependencies and clear ownership make testing and change review easier.

An interface does not automatically provide transactional safety, authorization, or performance. A repository returning an unbounded IQueryable can leak provider/lifetime concerns into every caller. Decide what is a trusted entry point, what data is borrowed or copied, and where effects commit. Preserve those contracts when swapping implementations.

A release decision links evidence to a public promise

Record the reason, alternatives, compatibility impact, migration/deployment order, tests, operational signals, and rollback/roll-forward conditions. A new schema, cache policy, auth claim mapping, or JSON option can require coordination even when method signatures stay the same. Validate actual runtime/package support against the explicit baseline, not a latest/preview setting.

State which checks ran and which remain outside scope. This course validates safe local framework components and synthetic calculations only; it does not start an existing configured application, TestServer, configured DB, proxy, cloud, or deployment. A maintainable architecture makes those missing boundaries visible so later authorized work can prove them rather than assume them.

Common errors and fixes

A serializer setting changes without API review.

Why it happens
Internal configuration is mistaken for nonobservable behavior.
How to fix it
Test the public representation and migration impact.
Prevent it next time
Keep fixed DTO examples and error contracts.

A nonunique sort key produces unstable pages.

Why it happens
The cursor does not encode a complete order.
How to fix it
Add a stable tie-breaker and define concurrent-change semantics.
Prevent it next time
Test equal sort values and boundary records.

A returned interface hides an unbounded deferred provider query.

Why it happens
Lifetime and resource costs escape the operation boundary.
How to fix it
Expose bounded application contracts with documented execution.
Prevent it next time
Review ownership and provider leakage.

Check your understanding

1. Why can adding a non-nullable required request field break existing clients even if the server compiles?

Show worked solution

Old clients may omit the field, producing binding/validation failure or a default that changes domain meaning. Review presence/null semantics, defaults, versioning, documentation, and migration. Test old and new payloads through the real request path when validating compatibility. Compile-time server types do not describe what already deployed clients send.

2. What additional checks are needed before turning the local pagination example into an EF endpoint?

Show worked solution

Verify provider translation, indexes/query cost, authorization/tenant scope, stable unique ordering, cursor validation, cancellation, page-size limits, and data-change/isolation semantics. The in-memory lookahead proves only its local sequence calculation. Keep database and hosted HTTP evidence separate, and avoid returning an unrestricted IQueryable to the client-facing layer.

Key takeaways

  • Compatibility includes representation, status, authorization, ordering, and effects.
  • Bounded DTO and pagination contracts make provider behavior reviewable.
  • Architecture and release records must not claim validation that never ran.

Official references