Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Guided course · AI and Career Skills for .NET Developers

    Prompt Engineering for .NET Developers: A Practical Guide: back to the course

    Module 1 · Prompt Foundations · Lesson 2 of 18

    Anatomy of a Strong Developer Prompt: Context, Specificity, Constraints and Output Format

    Learning outcome

    You can take a vague request ("write a method that sums the orders") and rewrite it as a prompt that reliably produces code you would merge: the right platform, the right types, the business rules stated, the constraints explicit, and the answer in a shape you can check. You can name the parts of a strong prompt, say which ones matter most for a given task, and explain why "act as a senior engineer" on its own changes very little.

    Intuition

    A prompt is a ticket you hand to a fast colleague who has never seen your codebase. A good ticket says what is wanted, why, what the definition of done is, and what must not change. The model is not lazy or stupid when it guesses; it guesses because the ticket left a gap.

    A practical formula, kept from the earlier version of this course because it works:

    Context + Task + Relevant code + Constraints + Expected output

    flowchart TB
        C["Context: app type, .NET version, packages, architecture"] --> P["prompt"]
        T["Task: one verb, one artifact (fix, write, review, explain)"] --> P
        R["Relevant code: real types and signatures, the error"] --> P
        K["Constraints: business rules, what must not change, no new packages"] --> P
        O["Expected output: files by name, tests, format, length"] --> P
        P --> A["answer you can check"]

    Deep dive

    1. Context: the facts the model cannot know

    Tell itWhy it changes the answer
    App type (Web API, minimal API, worker, Blazor, console, library)Hosting model, DI, lifetime and error handling differ
    Target framework and language version (".NET 8, C# 12")Available APIs (TimeProvider, primary constructors, collection expressions)
    Data access (EF Core 8, Dapper, raw ADO.NET) and databaseQuery patterns, transactions, SQL dialect
    Architecture (layered, Clean Architecture, vertical slices)Where code goes and which project references which
    Test stack (xUnit, NSubstitute, Moq, Testcontainers)What the test code looks like
    Conventions (nullable enabled, warnings as errors, sealed by default)Small things that otherwise cost review time

    Most IDE assistants let you store this once in a repository instructions file, covered in Lesson 3, so you do not retype it.

    2. Task: one verb, one artifact

    "Help with orders" is not a task. "Write a method", "find the root cause", "review this diff for X", "explain this code to a new joiner" are tasks. Name the artifact precisely: an ASP.NET Core minimal API endpoint, an EF Core query, an xUnit theory, a Kubernetes probe. When a request has several parts, number them, or better, split them into separate prompts.

    3. Specificity: replace adjectives with facts

    The weakest words in developer prompts are adjectives: fast, clean, secure, robust, production-ready. Replace each with something checkable:

    VagueSpecific
    make it fastp95 under 50 ms for 10,000 rows; no per-row database call
    make it securecaller must own the order (tenant id from the token, never from the body)
    handle errorsinvalid input returns 400 problem details; duplicate email returns 409
    production-readynullable-clean, cancellation tokens passed through, logs contain no PII
    clean codematch the style of the attached InvoiceService.cs

    4. Constraints: what must not happen

    Constraints prevent the most common unwanted "helpfulness":

    • "Do not add NuGet packages."
    • "Keep the public signature unchanged."
    • "Do not change behavior; list anything that would change."
    • "Use decimal for money and InvariantCulture for parsing."
    • "Do not touch files other than X."

    Negative constraints work best when you also say what to do instead ("no DateTime.Now; inject TimeProvider").

    5. Expected output: make the answer checkable

    Ask for the shape you will review:

    • code by file name, only changed members, or a unified diff;
    • a list of assumptions it made;
    • tests that prove the rules;
    • a short "what could break" section;
    • a length limit for explanations ("at most 5 bullets").

    Roles and personas

    "Act as a senior .NET engineer" sets tone and level of detail, but it is not a substitute for facts. A role is worth keeping when it changes the lens: "review as an application security engineer" really does steer the answer toward authorization and injection. A role with no context is decoration.

    Ask for questions when the gap is real

    For bigger tasks add: "If something essential is missing, ask up to 3 questions before writing code; otherwise state your assumptions and continue." This gets you clarifying questions where they matter and avoids a chat that never produces code.

    Code example: one request, two prompts

    Before

    Text
    Write a C# method that sums the order totals in a CSV file.

    Typical output (shortened):

    C#
    public static double Total(string[] lines) => lines.Sum(l => double.Parse(l.Split(',')[1]));

    After

    Text
    Context: .NET 8 console tool, C# 12, nullable enabled. Runs on build agents in several countries.
    Task: write a parser and a total for order lines.
    Input line format: Id,Amount,PlacedAt   e.g.  A-100,1234.50,2026-10-01T09:30:00+05:30
    Rules:
    - Amount is money: use decimal, never double. Amounts always use '.' as the decimal separator.
    - PlacedAt has an offset; store it as UTC.
    - A malformed line throws FormatException that includes the line.
    Constraints: no NuGet packages, no regex.
    Output: one file with a record Order, Parse(string) and Total(IEnumerable<string>). Then list your assumptions.

    The program below runs both versions on a machine whose culture is German, the situation the first prompt never mentioned:

    C#
    using System.Globalization;
    
    // The same task, two prompts. "Before" is what a vague prompt typically returns; "After" follows the specific prompt.
    var lines = new[] { "A-100,1234.50,2026-10-01T09:30:00+05:30", "A-101,99.99,2026-10-01T23:10:00+05:30" };
    
    CultureInfo.CurrentCulture = new CultureInfo("de-DE");   // a developer laptop or build agent in Germany
    
    Console.WriteLine($"Before (double, current culture): total = {Before.Total(lines)}");
    Console.WriteLine($"After  (decimal, invariant):      total = {After.Total(lines).ToString(CultureInfo.InvariantCulture)}");
    Console.WriteLine($"After  first order date (UTC):    {After.Parse(lines[0]).PlacedAtUtc:O}");
    
    static class Before
    {
        // Output of: "write a C# method that sums the order totals in a CSV"
        public static double Total(string[] lines) => lines.Sum(l => double.Parse(l.Split(',')[1]));
    }
    
    static class After
    {
        // Output of the specific prompt: decimal money, invariant culture, DateTimeOffset kept as UTC, errors name the line.
        public sealed record Order(string Id, decimal Amount, DateTimeOffset PlacedAtUtc);
    
        public static Order Parse(string line)
        {
            var parts = line.Split(',');
            if (parts.Length != 3) throw new FormatException($"Expected 3 fields but got {parts.Length}: '{line}'");
            try
            {
                var amount = decimal.Parse(parts[1], NumberStyles.Number, CultureInfo.InvariantCulture);
                var placed = DateTimeOffset.Parse(parts[2], CultureInfo.InvariantCulture).ToUniversalTime();
                return new Order(parts[0], amount, placed);
            }
            catch (Exception e) when (e is FormatException or OverflowException or ArgumentException)
            {
                throw new FormatException($"Invalid order line: '{line}'", e);
            }
        }
    
        public static decimal Total(IEnumerable<string> lines) => lines.Select(Parse).Sum(o => o.Amount);
    }

    Output:

    Text
    Before (double, current culture): total = 133449
    After  (decimal, invariant):      total = 1334.49
    After  first order date (UTC):    2026-10-01T04:00:00.0000000+00:00

    The "before" code is not just imprecise, it is wrong by a factor of 100: in de-DE the . is a group separator, so 1234.50 parses as 123450 and 99.99 as 9999. The specific prompt did not make the model smarter. It removed four guesses: the number type, the culture, the time zone and the error behavior.

    Failure modes

    • Context dumping. Pasting 3,000 lines "for context" dilutes the important part. Paste what the change touches.
    • Hidden business rules. The model writes CRUD fast, but "email must be unique" or "premium discount applies at 1000 or more" is only in your head.
    • Several tasks in one prompt. The answer does half of each. Split them.
    • Adjectives instead of numbers. "Fast" and "secure" get generic advice.
    • No output shape. You get a wall of prose and code that is hard to review.
    • Role without facts. "Act as an expert" plus "fix this" still forces the model to guess.

    Interview angle

    "How do you write a good prompt for a coding task?" Answer with the formula and an example: "I give the facts it can't infer: app type, .NET version, packages and the real types. I make one task per prompt, state the business rules and constraints as testable statements, and ask for a checkable output: files by name, assumptions and tests. For example, 'sum the orders' becomes: decimal money, invariant culture, UTC, a FormatException naming the bad line, no packages. That removes the guesses that cause real bugs, like culture-dependent parsing."

    Practice questions

    1. Rewrite "make this endpoint secure" as a specific constraint for a multi-tenant API.

    Answer: For example: "The tenant id comes from the tid claim, never from the route or body; every query filters by it; a request for another tenant's order returns 404, not 403, so the order's existence is not leaked."

    1. Why is double.Parse(text) without a culture risky in code that runs on many machines?

    Answer: It uses the current culture. In cultures where . is a group separator, 1234.50 parses as 123450. Money should be decimal parsed with CultureInfo.InvariantCulture or an explicit culture.

    1. When is a role ("act as a security reviewer") genuinely useful?

    Answer: When it changes the lens of the answer, for example toward authorization and injection risks. It does not replace context such as versions, code and rules.

    1. What output format would you request for a refactor of a 300-line class?

    Answer: Only the changed members or a unified diff, a list of behavior changes (ideally "none"), assumptions, and tests that pin current behavior.

    1. How do you stop an assistant from asking ten questions before writing anything?

    Answer: Allow a small number of questions only when information is essential, and otherwise ask it to state assumptions and continue.

    Revision checklist

    • Context + Task + Relevant code + Constraints + Expected output.
    • Platform facts first: app type, .NET version, packages, architecture.
    • One verb, one artifact per prompt.
    • Replace adjectives with numbers and rules.
    • Say what must not change, and what to do instead.
    • Ask for a checkable answer: files, assumptions, tests, risks.

    Analogy

    Everyday picture

    A good bug ticket

    "It's broken" gets you a shrug. "On build 1.4.2, POST /orders with an empty cart returns 500; expected 400; logs attached" gets you a fix the same day. Prompts are tickets.

    Quick reference

    Prompt skeleton

    • Context: app type, .NET 8 / C# 12, packages, architecture.
    • Task: one verb + exact artifact.
    • Code: real types, signatures, error text.
    • Rules and constraints: testable; what must not change.
    • Output: files by name, assumptions, tests, risks; length limit.
    Sign in to mark lessons done and keep your place in the course.Sign in