Concepts
What you will learn: Use an assistant to speed up work while keeping behavioral review and responsibility with the engineer. Turn a small request into a spec, provide useful context, then verify the plan, code and data behavior.
Before you start: Basic C#, dependency injection, async and tests are useful. A prompt is the instruction and input sent to a model; context is the code, constraints and information available to it; a diff is the proposed file change. Model-training experience is unnecessary, but you should be able to explain code you accept.
Select a term with a dotted underline for its meaning, then continue reading from the same place.
#Why fundamentals still matter
can generate an endpoint quickly while missing order ownership or charging twice after a retry. Engineering includes understanding , async execution, databases and security well enough to assess behavior beyond the happy path.
Ask for decision explanations, then verify them against source, generated and tests. If you cannot explain the failure path or data changes, reduce the task before merging. Keep process proportional to risk; a tiny edit does not need a lengthy planning ceremony.
#Spec-driven development from intent to evidence
- Rather than requesting an orders endpoint, specify inputs, authorized callers, expected output and failures: users see only their own orders, in a stable order with bounded pages.
- Before implementation, map each requirement to an implementation location and verification method. Missing ownership checks or page limits become visible while the change is still small.
- After implementation, compare behavior with the requirements and verify rejection paths. A successful build does not establish correct permissions or business rules.
“Add order cancellation” leaves ownership, allowed states and payment behavior undefined. Follow spec → plan → tasks → implement → review. The spec defines testable behavior, the plan chooses an implementation, and tasks divide it into reviewable changes.
Feature: cancel an unpaid order
Actor: authenticated customer who owns the order
POST /orders/{id}/cancel
Pending -> Cancelled; repeat cancellation returns the same successful outcome.
Paid or Shipped -> 409; unknown or another customer's order -> 404.
One transaction changes state, releases reserved stock and writes an outbox event.
Concurrent payment and cancellation cannot both win the state transition.
Non-goals: refunds, shipping cancellation, a new messaging framework.
Acceptance: ownership, repeat calls, concurrent transition and rollback tests.
- Review the spec with the decision owner, especially payment versus cancellation. A late payment after cancellation needs an explicit reconciliation path.
- The plan identifies conditional transitions, boundaries and unique constraints.
- Implement one working slice with a test, then concurrency cases and telemetry. Avoid building months of abstractions before observable behavior.
- Review the diff against acceptance criteria and run checks independently of the model's explanation.
Update the spec when requirements change; do not leave a new business rule hidden in implementation.
#How to review an AI plan
A persuasive plan can still omit a crucial requirement. Review concrete scenarios before class names.
| Check | Required evidence |
|---|---|
| Requirements coverage | Each criterion maps to a step and a test |
| Data model | Keys, constraints, tenant ownership and a safe migration |
| Atomic writes versus external effects needing or reconciliation | |
| Security | Trusted identity, resource authorization and secret handling |
| Failure modes | Timeouts, duplicates, partial failure and concurrent requests |
| Tests | Tests that catch the actual regression using a suitable provider |
| Scope creep | Every dependency and abstraction serves a present requirement |
Request a simpler alternative when a local operation gains a or one task gains a generic framework. Ask what happens if execution stops after each consequential step, and identify recovery.
#Context that reduces guessing
Provide the spec, relevant files, an existing implementation to follow, and package versions, and verification commands. An AGENTS.md-style file is useful for stable rules, but neither guarantees compliance nor belongs in secret storage.
# Repository guidance
Target .NET 10; follow the package versions already pinned in this repository.
Read the cancellation spec and the existing order endpoint before editing.
Derive CustomerId from authenticated identity; never trust the request body.
Keep order, reservation and outbox writes in one database transaction.
Change only the order cancellation slice. Explain any new dependency first.
Run the relevant integration tests and report the commands and outcomes.
Do not include customer records, tokens or connection strings in prompts.
Tests should contract observable behavior, not implementation details. Include another customer's cancellation attempt and concurrent transitions. Ask for a small diff and explicit reporting of executed and skipped checks. Long conversations can contain stale context; use repository files as the source of truth.
#A .NET review checklist and data mistakes
Review behavior and edge cases, ownership and security, and concurrency, async and lifetimes, query shape and performance, then readability and tests. The examples are educational fragments: db is a scoped , ct is the request , and customerId comes from authenticated identity.
| Mistake | Symptom | Verification |
|---|---|---|
| One query per order | Inspect and call count | |
| Missing AsNoTracking | Unneeded tracking for entity reads | Check whether updates are intended |
| FromSqlRaw interpolation | Input becomes SQL syntax | Verify parameterization |
| Missing ownership | Knowing an ID grants access | Test different users |
var orders = await db.Orders.ToListAsync(ct);
foreach (var order in orders)
await db.Entry(order).Collection(o => o.Lines).LoadAsync(ct);var summaries = await db.Orders
.Where(o => o.CustomerId == customerId)
.Select(o => new { o.Id, LineCount = o.Lines.Count }) // (1)
.ToListAsync(ct);var products = await db.Products.ToListAsync(ct);var products = await db.Products.AsNoTracking().ToListAsync(ct); // (2)var products = await db.Products
.FromSqlRaw($"SELECT * FROM Products WHERE Name = '{name}'")
.ToListAsync(ct);var products = await db.Products
.FromSql($"SELECT * FROM Products WHERE Name = {name}") // (3)
.AsNoTracking().ToListAsync(ct);- (1) counts in the query instead of loading all lines; inspect provider translation.
- (2) Remove tracking when entities will not be updated. Scalar-only projections do not track entities in the first place; AsNoTracking is not required on every query.
- (3)
FromSqlaccepts the interpolated value as parameters; do not convert it to a string first. Table and column identifiers need an allowlist, not value parameters.
#Async, cancellation and DI lifetimes
| Mistake | Risk |
|---|---|
| .Result and .Wait | Blocking and pool starvation; possible with a synchronization context |
| async void | No awaitable task for completion and failure |
| Missing CancellationToken | continues after the request ends |
| Captive dependency | A singleton retains a scoped dependency beyond its lifetime |
var order = LoadOrderAsync(id, ct).Result;
SaveAsync(ct).Wait();var order = await LoadOrderAsync(id, ct); // (1)
await SaveAsync(ct);public async void SaveOrder() => await db.SaveChangesAsync();public async Task SaveOrderAsync(CancellationToken ct) // (2)
=> await db.SaveChangesAsync(ct);var orders = await db.Orders.ToListAsync();var orders = await db.Orders.ToListAsync(ct); // (3)services.AddSingleton<OrderReader>();
// OrderReader constructor receives a scoped StoreDbContext.services.AddScoped<OrderReader>(); // (4)
// A singleton worker instead creates a scope per unit of work.- (1) Await frees threads during I/O; wrapping a query in Task.Run is unnecessary.
- (2) Callers can observe completion and exceptions. Async void belongs in event handlers whose contract requires void.
- (3) Cancellation is cooperative. After a sensitive side effect, use an explicit recovery protocol rather than assuming cancellation rolled it back.
- (4) Request services can be scoped. A long-lived worker uses IServiceScopeFactory or IDbContextFactory and a separate context per unit of work.
#Exceptions, time and HttpClient
| Mistake | Review question |
|---|---|
| Catching Exception | Is there a recovery policy, and is cancellation handled correctly? |
| Swallowed errors | Is failure being presented as success? |
| DateTime.Now | Is this an instant or local civil time, and is it testable? |
| new HttpClient per call | Are connection pooling and lifecycle managed? |
try { await gateway.ChargeAsync(order, ct); }
catch (Exception) { return PaymentResult.Declined; }try { await gateway.ChargeAsync(order, ct); }
catch (PaymentDeclinedException) // application-defined exception
{
return PaymentResult.Declined; // (1)
}try { await db.SaveChangesAsync(ct); }
catch (Exception) { }
return true;await db.SaveChangesAsync(ct); // (2)
return true;order.CreatedAt = DateTime.Now;order.CreatedAt = timeProvider.GetUtcNow(); // (3): DateTimeOffset propertyusing var client = new HttpClient();
var text = await client.GetStringAsync(url, ct);// At startup:
services.AddHttpClient("payments");
// In a service receiving IHttpClientFactory:
var client = factory.CreateClient("payments"); // (4)
var text = await client.GetStringAsync(url, ct);- (1) Handle a known decline only; a timeout may conceal a successful charge needing reconciliation. Broad catches can be valid at global or job boundaries with explicit reporting and failure policy.
- (2) Failure reaches the caller. Log once at an appropriate boundary when needed; do not return success.
- (3) represents an instant, and TimeProvider enables controlled tests. Local booking schedules need time-zone rules rather than indiscriminate UTC conversion.
- (4) The factory manages handlers. A long-lived HttpClient with PooledConnectionLifetime is also valid; the issue is unmanaged per-call lifetimes.
#Hallucinated APIs and over-abstraction
Plausible code may use a nonexistent or install an invented package. Verify name, publisher and version through official sources before restoring; a familiar name is not proof of safety. These two warning signs deserve a pause.
| Mistake | Evidence | Response |
|---|---|---|
| Hallucinated API or package | No symbol in the installed version or trusted source | Check documentation and metadata, then build |
| Over-abstraction | Layers without a present requirement | Use direct implementation until real variation emerges |
// Deliberately invalid: do not install a package to make this name work.
var orders = await db.Orders.FetchAllSafelyAsync(ct);var orders = await db.Orders.AsNoTracking()
.Where(o => o.CustomerId == customerId).ToListAsync(ct);public interface IOrderTotalStrategy
{
decimal Calculate(decimal subtotal, decimal delivery);
}
// Plus a factory and resolver for a single implementation.public static decimal CalculateTotal(decimal subtotal, decimal delivery)
{
ArgumentOutOfRangeException.ThrowIfNegative(subtotal);
ArgumentOutOfRangeException.ThrowIfNegative(delivery);
return subtotal + delivery;
}Introduce interfaces for real boundaries, variation or justified testing needs. Understand consumers before removing existing abstractions. Building is necessary but insufficient: real APIs can still be misused.
#Security, licenses and measuring quality
Before submitting code or data to an service, check organizational policy, retention and training settings, and processing location. Use synthetic data and exclude and connection strings. Review dependencies, lockfiles and licenses for the selected versions, with legal requirements handled by the appropriate specialist.
MediatR and AutoMapper changed licensing in 2025, and FluentAssertions v8 introduced commercial requirements for certain uses. Read the actual license and exemptions rather than assuming every version is free or every use requires payment. Assertion alternatives include AwesomeAssertions and Shouldly. These changes are a reason to verify terms, not a legal determination.
| Measure | Useful approach |
|---|---|
| Correctness | Acceptance pass rate and severity of escaped defects |
| Delivery | Task time including review and rework |
| Maintenance | Diff size, clear responsibilities and explainable code |
| Operations | , query counts and production failures |
| Cost | Tool spend plus review effort per accepted change |
Lines and prompt counts do not measure productivity. Compare tasks of similar scope and risk, recording confounding factors.
AutoMapper and MediatR announcement · FluentAssertions license
Interview questions
No results
Check your reasoning
Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?
31.1Why do fundamentals matter if AI can write the code?Junior
I still need fundamentals because generated code can compile while violating business rules. A compiler cannot tell whether a customer owns an order, whether two requests can oversell stock, or whether a retry charges twice. I use to accelerate exploration and implementation, then apply my understanding of databases, concurrency and security to review the result. I only accept code I can explain and support. The team remains responsible for what it ships, regardless of who produced the first draft.
31.2What does spec-driven development mean in practice?Mid
I start with a spec describing observable behavior, constraints, failure cases and explicit non-goals. I review a plan that maps those requirements to data changes, boundaries and tests, then implement small vertical slices. Review compares the resulting behavior and diff with the spec rather than merely checking whether the code looks sensible. When a requirement changes, I update the spec and tests together. I keep the ceremony proportional to the risk and size of the change.
31.3How would you review an AI-generated implementation plan?Mid
I check that every acceptance criterion has an implementation step and meaningful verification. Then I inspect the data model, constraints, ownership checks, boundaries and external side effects. I walk through duplicate requests, concurrent changes, timeouts and partial failure. I also ask whether each new package and abstraction solves a present requirement. If the plan is much larger than the feature, I request a simpler alternative. A plan is ready when its important assumptions and recovery paths are explicit.
31.4What context should you give an AI coding assistant?Junior
I provide the current requirement, non-goals, relevant files, repository conventions and the versions already in use. I include a working example from the codebase and the exact checks needed for the change. Stable guidance belongs in a short repository instruction file, while task-specific decisions belong with the spec. I avoid secrets and customer data, and prefer focused context over dumping the whole repository. I also ask the assistant to identify missing information rather than invent or requirements.
31.5Why is a passing build insufficient evidence for AI-written code?Mid
A build checks syntax, references and type rules, but it cannot prove the intended behavior. An endpoint can compile and still leak another customer's data, issue an query pattern or return success after swallowing a failure. I require acceptance tests for the business behavior, integration tests where database semantics matter, and review of security and failure paths. I inspect the actual diff and relevant runtime evidence. A model's claim that everything is correct is not an independent check.
31.6How do you catch N+1 and unnecessary tracking?Mid
I inspect loops and access, then verify generated and command counts with representative data. I usually project the response shape in the database rather than loading entire graphs. For read-only entity queries, AsNoTracking avoids unnecessary change tracking. I do not add it mechanically to scalar projections, which do not track entities anyway, or to workflows that intentionally update tracked entities. The important evidence is bounded database work and the correct update behavior, not simply the presence of a particular method call.
31.7What async mistakes do you look for in generated .NET code?Mid
I look for .Result and .Wait, async void outside event handlers, missing awaits and cancellation that stop at an intermediate method. I also check for fire-and-forget work using request-scoped services and concurrent operations on one . In , blocking tasks can starve the thread pool even without the classic synchronization-context . I use genuine async throughout the call chain and durable background processing for work that must survive the request or process.
31.8What is a captive dependency?Mid
A captive dependency occurs when a longer-lived service retains a dependency that should have a shorter lifetime. A singleton holding a scoped is a common example: the context can outlive its intended unit of work and be accessed concurrently. I review registrations as well as constructors. A request service can become scoped, while a singleton background worker can create a scope or use a context factory per operation. Making the DbContext singleton does not solve the problem.
31.9How do you review FromSqlRaw for injection risks?Mid
I check whether untrusted values are concatenated or interpolated into a string passed to FromSqlRaw. That makes them part of syntax and creates an injection risk. I use a parameterizing such as FromSql with an interpolated string, or explicit parameters for raw SQL. Dynamic identifiers cannot be parameterized as values, so they need a strict allowlist. Parameterization does not replace authorization: the query still has to restrict results to the authenticated user's permitted data.
31.10When is catching Exception acceptable?Senior
I allow broad exception handling at a responsible boundary, such as an exception handler or a background job runner, when it records failure and applies a clear policy. Inside business logic I prefer specific exceptions only when I can recover or translate a known outcome. I do not turn an unknown payment failure into a decline, because the payment may have succeeded. Cancellation should be handled deliberately, and exceptions should not be swallowed or logged repeatedly at every layer.
31.11How do you make time-dependent generated code testable?Mid
I inject TimeProvider instead of reading machine time throughout the code. For timestamps representing instants, I normally use GetUtcNow and DateTimeOffset, then control the clock in tests for expiry boundaries and retries. I distinguish that from local civil schedules, such as a booking at nine in a particular time zone, which need zone and daylight-saving rules. Replacing every time value with mechanically can lose business meaning; the representation should match the domain.
31.12How do you handle hallucinated packages and licensing risks?Senior
I verify proposed packages against official documentation, trusted registries and the publisher's repository before adding them. I check the exact version, transitive dependencies, security advisories and license terms rather than assuming a familiar library remains free under every version. An invented package name can become a supply-chain risk if someone publishes it. I keep changes explicit in the dependency diff and escalate unclear licensing requirements to the appropriate reviewer. Successful compilation alone does not establish provenance or acceptable terms.
31.13How do you prevent AI-generated over-engineering?Senior
I define non-goals and ask how each new layer, abstraction or dependency supports a current requirement. I prefer a small working slice with observable behavior before generalizing. If there is only one implementation and no meaningful boundary, a direct method may be clearer than a factory and strategy hierarchy. I still preserve existing architectural boundaries where they serve real consumers. The goal is a change that is easy to review and maintain, with complexity justified by evidence rather than hypothetical future flexibility.
31.14What makes a useful test contract for an AI task?Mid
I write acceptance criteria around observable outcomes and important . For order cancellation, that includes ownership, repeated requests, incompatible states, concurrent payment and rollback of related writes. Tests should fail if those rules break, without mirroring private helper calls or the current class layout. I use the real database provider where or query translation matter. I also review test assertions, because generated tests can pass while checking nothing important or reproducing the same misunderstanding as the implementation.
31.15How would you measure whether AI improves engineering productivity?Senior
I measure delivery time including review and rework, alongside acceptance quality, escaped defects and operational outcomes. I include tool spend and maintenance effort rather than counting only the time to generate a draft. Comparisons need similar task size and risk, with enough observations to avoid drawing conclusions from one easy change. I also check whether the team can explain and modify the result. More generated lines or more accepted suggestions can coexist with worse engineering outcomes.
Test yourself
Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.
Which spec is most useful for an AI implementer?
What is wrong with FromSqlRaw($"... Name = '{name}'")?
A singleton worker needs DbContext. What should it do?
When is AsNoTracking especially useful?
Why inject TimeProvider?
A generated package name looks plausible. Next step?
Which result best indicates an AI workflow improvement?
A payment call times out. What should review reject?
What is the strongest evidence that an AI change meets the task?
Which HttpClient lifetime approach is valid?