Hello. You have just decided how the portfolio API should evolve when a currency-aware summary breaks external clients: retain a measured v1 contract, introduce v2, protect both with contract tests, and record version-level telemetry for eventual retirement.
This lesson makes that last point practical while building a broader senior-engineering habit: use an AI assistant to accelerate a small, bounded change, but treat its output as an untrusted pull-request contribution. You will define the change and its constraints, have the assistant propose an implementation, and independently establish whether it is correct, secure, adequately tested, and documented. This is a Tier 3 skill: valuable in interviews and daily delivery, but it should not delay applications.
AI assistance changes speed, not accountability
An AI assistant can read nearby code, propose a patch, generate tests, and identify likely issues. It cannot own the consequences of a production change. It may misunderstand a business term, infer a convention that does not exist, select an obsolete API, or produce code that appears idiomatic but violates a non-obvious security or operational requirement.
Use this operating assumption:
AI output is a draft from a fast contributor who has not attended your design review and does not have production accountability.
That framing avoids two bad extremes:
- Blind acceptance: “It compiles, so it must be correct.”
- Blanket rejection: “AI is unreliable, so it has no useful role.”
The professional middle ground is to delegate a narrow task, provide relevant constraints, inspect the diff, and gather evidence before merge. The same standard applies whether the author is Copilot, a teammate, or you returning to a change after several weeks.
The supplied review image illustrates the point well: Copilot identifies both a likely typo (scor rather than score) and an inconsistent penalty change. Those are plausible review findings, but neither should be applied solely because an AI marked it “High.” Verify the surrounding game rule, identify the intended source of truth, and make sure a test expresses that rule. A suggested patch is not evidence.

For the capstone, we will use a deliberately constrained change that continues the API-versioning decision:
Add the structured log-scope property
ApiContractVersionto requests whose paths begin exactly with/api/v1/or/api/v2/. Do not change authentication, authorization, request handling, response bodies, status codes, database behavior, or API contracts.
This is a useful exercise because it looks small but has real delivery concerns:
- Correctness:
/api/v1/portfolios/...should be labelledv1;/api/v10/...must not be incorrectly labelledv1. - Security: version labels are safe low-cardinality telemetry, but request bodies, authorization headers, bearer tokens, and portfolio values must not be added to logs.
- Operational behavior: the scope should be active for downstream application logs, including logs produced while processing a failing request.
- Testing: tests must cover the supported and unsupported path cases, not merely assert that the code compiles.
- Documentation: engineers need to know the property exists and how it supports the
v1retirement decision.
Before using any assistant, make the scope explicit in your own words. If you cannot explain the change in one or two sentences and name what it must not affect, it is too broad to delegate safely.
Give the assistant bounded context, not an open-ended command
A vague request such as “Add observability to API versioning” invites broad, inconsistent changes: new packages, logging configuration edits, controller rewrites, or speculative dashboards. Instead, tell the assistant the intended outcome, allowed files, constraints, and acceptance criteria.
Here is a suitable prompt for Copilot Chat, VS Code, or Visual Studio. Adjust file names to your actual solution.
Propose, but do not apply, a minimal .NET 8 change in src/Portfolio.Api that
adds a structured logging scope property named ApiContractVersion.
Behavior:
- Set it to "v1" only for paths beginning with /api/v1/ and "v2" only for
paths beginning with /api/v2/.
- Do not classify /api/v10/, /api/version/, or unrelated paths.
- Preserve all existing HTTP responses, authentication, authorization, and
exception behavior.
- Do not log request or response bodies, headers, tokens, user identifiers,
portfolio values, or connection strings.
- Add no NuGet packages and no new external dependencies.
- Keep the change limited to middleware registration, a small testable
middleware or helper, tests, and concise documentation.
First give:
1. assumptions and files to change,
2. a unified diff,
3. the tests you recommend,
4. risks or ambiguity you need me to resolve.
Do not delete, skip, weaken, or modify existing tests to make the patch pass.
Several details matter.
State constraints as negatives as well as positives
“Add a log scope” says what you want. “No headers, tokens, identities, bodies, package additions, or behavior changes” narrows how it may be done. Negative constraints are especially valuable for authentication, finance, and deployment code because a seemingly helpful implementation can create an unnecessary exposure.
Ask for assumptions before implementation
The assistant may assume that the application uses controller routing, has Serilog, or has an existing middleware convention. It may be wrong. Requiring it to state assumptions gives you a review surface before code exists.
A good response might say:
- It assumes the API uses
ILogger<T>and standard ASP.NET Core middleware. - It assumes the version is represented in URI paths, matching the previous API-versioning decision.
- It proposes no package because
ILogger.BeginScopeis part of the existing logging abstraction. - It needs confirmation about whether
v1andv2routes have a trailing slash convention.
An unsafe response might claim to “automatically extract the version” using a broad regular expression without defining edge cases, add a logging package without need, or choose API version from an untrusted custom header that your contract does not use.
Supply durable repository context
Repeatedly explaining team conventions wastes time and still produces inconsistent code. A repository-level Copilot instructions file can define stable expectations such as:
# Engineering instructions
- Target .NET 8 and nullable reference types.
- Use xUnit for tests and Arrange, Act, Assert structure.
- Preserve public API contracts unless a task explicitly authorizes a change.
- Never log authorization headers, access tokens, connection strings,
portfolio values, or personally identifiable information.
- Prefer existing dependencies. Explain and obtain approval before adding one.
- Do not delete, skip, or weaken tests to make a change pass.
- Add or update concise documentation for operationally visible behavior.
Keep instructions factual and reviewable. They should encode team decisions, not attempt to replace a task specification. The task prompt still supplies the current requirement, affected files, and acceptance criteria.
Smaller prompts, better answers with GitHub Copilot Custom Instructions
Watch “Smaller prompts, better answers with GitHub Copilot Custom Instructions” from the Visual Studio Code channel for a compact demonstration of shared custom instructions and why they make prompts more consistent.
Watch shared instructions. Focus on the idea of checking team conventions into the repository so that they are available to collaborators, rather than treating instructions as private personal preferences. Apply that idea to test conventions, security boundaries, and API-contract rules—not merely formatting.
Do not put secrets, production customer data, access tokens, connection strings, private incident reports, or proprietary data that your organization prohibits sharing into prompts. Follow your employer’s approved AI-tool and data-handling policy even when an IDE makes pasting context easy.
Inspect the proposed patch for semantic correctness
After the assistant proposes a diff, do not start with “Apply.” Read it as you would a pull request. Begin at the boundaries: what enters the middleware, what it changes, when it runs, and what happens on error.
A minimal implementation might reasonably have this shape:
public sealed class ApiContractVersionScopeMiddleware
{
private readonly RequestDelegate _next;
public ApiContractVersionScopeMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(
HttpContext context,
ILogger<ApiContractVersionScopeMiddleware> logger)
{
var version = GetVersion(context.Request.Path);
if (version is null)
{
await _next(context);
return;
}
using (logger.BeginScope(new Dictionary<string, object?>
{
["ApiContractVersion"] = version
}))
{
await _next(context);
}
}
internal static string? GetVersion(PathString path)
{
var value = path.Value;
if (value is null)
{
return null;
}
if (value.StartsWith("/api/v1/", StringComparison.OrdinalIgnoreCase))
{
return "v1";
}
if (value.StartsWith("/api/v2/", StringComparison.OrdinalIgnoreCase))
{
return "v2";
}
return null;
}
}
The example is intentionally modest. Its purpose is not to create a logging framework; it is to attach one low-cardinality, contract-relevant property to applicable requests.
Now review it against the acceptance criteria.
1. Verify path semantics, especially near misses
The exact trailing slash is meaningful in this implementation:
| Request path | Expected ApiContractVersion | Reason |
|---|---|---|
/api/v1/portfolios/42/summary | v1 | A valid v1 API request. |
/api/v2/portfolios/42/summary | v2 | A valid v2 API request. |
/api/v10/portfolios/42 | Absent | v10 is not v1. |
/api/version/status | Absent | Not a versioned contract route. |
/health | Absent | Health traffic should not be labelled as an API contract version. |
/api/v1 | Decide deliberately | It may be an invalid route, but your convention must be explicit. |
If your API permits a valid endpoint at exactly /api/v1, revise the helper and tests accordingly. Do not let an assistant decide this implicitly. The important principle is not the specific string operation; it is that your classification is precise and demonstrated by tests.
2. Verify middleware order, not just middleware code
The requirement says the scope should be available during the request. Placement determines what “during” means.
A defensible arrangement for the stated path-based implementation is:
app.UseMiddleware<ApiContractVersionScopeMiddleware>();
app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Because the version comes only from a fixed path prefix, the middleware can execute before routing. Putting it outside the exception-handling middleware also allows logs emitted by the exception handler to inherit the version scope when an applicable request fails.
This is a design choice, not a universal ordering rule. If your exception handler is configured differently or the API version is selected from endpoint metadata rather than path, reassess the order. The key review question is:
Does the property exist for every log event you need it to enrich, without changing the security or error-handling behavior of the pipeline?
Avoid adding a try/catch merely to log and rethrow unless there is a clear logging need and the exception is rethrown without alteration. An unnecessary catch often produces duplicate error logs or interferes with the application’s established exception-to-Problem-Details mapping.
3. Verify scope lifetime across asynchronous work
The scope must surround await _next(context). If the assistant creates a scope and disposes it before awaiting the next delegate, downstream logs will not carry the property. If it calls .Wait(), .Result, or GetAwaiter().GetResult(), reject it: those are sync-over-async calls and violate the request-path standard established earlier in this course.
The using block in the example remains active through the asynchronous continuation and is disposed afterward. That is the behavior wanted here.
4. Check that the change is actually minimal
Ask:
- Did it alter controller routes or version-selection behavior?
- Did it add a package that the platform already provides?
- Did it introduce a global logging change that might expose more data?
- Did it edit unrelated files, reformat broad sections, or make a surprise refactor?
- Did it add code you cannot explain line by line?
A “minimal diff” is a risk-control mechanism. It reduces the chance of hidden regressions and makes human review meaningful.
Verify with evidence: tests, analysis, security, and documentation
Compilation is necessary but weak evidence. A robust review combines automated checks with human inspection and a small amount of adversarial thinking.
Read GitHub’s practical review guidance to turn the AI patch into an evidence-based pull-request review rather than a trust exercise.
In “1. Start with functional checks,” read the functional-check guidance. Then read “2. Verify context and intent,” beginning with context and intent; compare its questions with the constraints in your prompt. In “4. Scrutinize dependencies,” read dependency review, even though this change prohibits dependencies. Finish with “5. Spot AI-specific pitfalls,” especially AI-specific failure modes.
A verification ladder
Use the following sequence for this constrained change. Each layer answers a different question.
| Layer | Evidence to collect | What it proves |
|---|---|---|
| Build | dotnet build with no new warnings | The patch uses valid APIs and references. |
| Focused unit tests | GetVersion recognizes v1, v2, and near misses | Path classification matches the stated contract. |
| Integration test | A test host issues a versioned request and captures an application log from downstream middleware or endpoint | The log scope actually flows through the HTTP request path. |
| Regression suite | dotnet test for the solution | Existing behavior still holds. |
| Static and security checks | Existing analyzers, dependency review, CodeQL or repository scanning in CI | Common quality and vulnerability issues have been checked. |
| Manual API smoke check | Call a v1 route, v2 route, invalid route, and health endpoint locally | The observable behavior matches expectations. |
| Documentation review | A concise operational note is accurate and discoverable | Future maintainers can use the property responsibly. |
The first lesson in testing will cover strategy and test depth in more detail. For now, do not confuse a generated test with a meaningful test. A test that calls GetVersion("/api/v1/orders") and expects v1 is useful but incomplete. The bug most likely to escape is overly broad matching, so /api/v10/orders and /api/version/orders are high-value cases.
If the assistant generates tests, inspect them for these red flags:
- They reproduce the implementation rather than assert the requirement.
- They only test success cases.
- They assert internal implementation details but never observable behavior.
- They use arbitrary sleeps or depend on test order.
- They are skipped, deleted, or weakened to make the change appear green.
- They claim to test scope propagation but do not configure a log provider capable of observing scope values.
For .NET projects, Copilot can help propose tests for a class, file, or current Git change. That is useful for coverage discovery, but generated tests still require the same review.
Generate and run unit tests using GitHub Copilot testing - Visual Studio (Windows) | Microsoft Learn
Read Microsoft Learn’s workflow for asking Copilot to generate .NET tests, then use it as a second opinion on missing cases rather than as automatic proof of quality.
Start with “Ways to start GitHub Copilot testing” and the entry points. In “Prompt syntax,” focus on scoped test prompts, particularly the option to target current Git changes. Then read “Generate and run tests” from generation through Test Explorer. A useful prompt after reviewing your own test plan is: @Test #git_changes using xUnit; cover valid v1 and v2 paths, v10 and non-API near misses, and do not modify existing tests.
Security review for this specific change
Security review is not limited to finding SQL injection or authentication bugs. For logging changes, focus on data disclosure and audit usefulness.
Check all of the following:
- Allowed data only:
ApiContractVersionhas a tiny fixed set of values, such asv1andv2. It is low-cardinality and non-sensitive. - No raw request data: reject changes that log
Request.Pathwholesale if route parameters could contain sensitive values, and reject logging headers, query strings, bodies, tokens, or claims unless separately approved and redacted. - No authorization change: logging version information must not influence authentication or authorization decisions.
- No exception leakage: the change must not expose exception details to the client or bypass your existing Problem Details handling.
- No unreviewed dependency: if an assistant adds a logging, regex, or telemetry package, establish why the platform capability is insufficient; verify package identity, maintenance, license, and vulnerability status before adding it.
- No secret in the prompt or diff: inspect configuration changes and generated test fixtures for connection strings or copied credentials.
Documentation is part of the change
The documentation can be short because this does not change a public HTTP contract. Add a note to an operations or API-versioning decision document, for example:
## API-version telemetry
For requests beginning with `/api/v1/` or `/api/v2/`, the API adds the
structured log property `ApiContractVersion` with values `v1` or `v2`.
Use this property with authenticated client identity, status code, and latency
to measure remaining v1 use during the published deprecation window. Do not
use it to log request bodies, credentials, portfolio values, or raw tokens.
That note answers three operational questions: what the property means, when it appears, and how it must not be misused. It also closes the loop with the previous lesson: version retirement is based on evidence, not guesswork.
How to automate code reviews and testing with GitHub Copilot
Watch “How to automate code reviews and testing with GitHub Copilot” from GitHub for a pull-request-level example of treating AI-generated work as reviewable code subject to normal checks.
Watch repository guardrails to see the relationship between review rules, required tests, code scanning, and repository instructions. Then watch human verification. Focus on the final discipline: inspect the implementation locally and require automated checks to pass before accepting an AI-authored change.
A repeatable review record for AI-authored changes
For your capstone, create a small pull request or local branch such as chore/api-version-log-scope. Keep the assistant’s proposal visible in the conversation history or PR description. Then record the evidence in a concise template:
## AI-assisted change review
### Change
Add `ApiContractVersion` structured log scope for `/api/v1/` and `/api/v2/`.
### Constraints verified
- No API response, routing, authorization, or database behavior changed.
- No request body, header, token, portfolio value, or identity data logged.
- No external packages added.
### Correctness evidence
- Unit tests cover v1, v2, v10, unrelated API-like paths, and non-API paths.
- Integration test confirms downstream log scope for a versioned request.
- Full test suite passed.
- Local smoke requests confirmed unchanged HTTP responses.
### Security and dependency evidence
- Existing static analysis and repository security checks passed.
- No dependencies changed.
- Diff reviewed for secret or sensitive-data exposure.
### Documentation
- API-version telemetry note added or updated.
### Decision
Accepted / revised / rejected, with reason.
A review record is not bureaucracy for its own sake. It creates a defensible explanation in a code review, incident investigation, or interview:
I use AI for bounded implementation and test generation, but I define invariants first. I inspect the diff for architecture and security boundaries, run focused and regression checks, verify observable behavior, and document operational consequences. I do not treat generated tests or a green build as sufficient proof.
That answer is considerably stronger than “I use Copilot to write code faster.” It describes judgment, risk management, and an engineering process that scales to a team.
Key takeaways
- AI-generated code is a draft, not an authority; accountability for correctness and safety remains with the engineer and reviewer.
- Constrain AI work with an explicit goal, non-goals, allowed scope, acceptance criteria, and a request to state assumptions before editing.
- Review the diff semantically: verify path edge cases, middleware order, asynchronous scope lifetime, and absence of unrelated change.
- Gather layered evidence through focused tests, integration behavior, the full regression suite, static/security checks, manual smoke checks, and documentation review.
- Treat logging as a security-sensitive feature: use fixed, low-cardinality fields and never casually log tokens, headers, bodies, identities, or financial data.
- Repository instructions improve consistency, but they supplement rather than replace a specific task contract.
This completes the modern AI-assisted delivery practice for the API module. The next module moves into EF Core, SQL Server, and Angular refreshers, beginning with shaping an efficient EF Core read query through projection, an explicit tracking choice, and avoidance of N+1 access patterns.
Can't find a good explanation? Sign up and we'll make it for you
Sign up