Prompt Engineering for Developers: Getting the Most from AI Coders

44 min read

Provide actionable tips and best practices for crafting effective prompts to maximize the usefulness of AI coding assistants.

Prompt Engineering for Developers: Getting the Most from AI Coders

The art of talking to machines so they actually listen (and write decent code)


Introduction: The Art of AI Whispering

Picture this: You're sitting at your computer at 2 AM, staring at a blinking cursor, and you decide to ask your AI coding assistant for help. You type "write me some code" and get back something that looks like it was written by a caffeinated intern who just discovered design patterns. Sound familiar?

Welcome to the wonderful world of prompt engineering—the surprisingly nuanced art of communicating with AI systems in a way that produces useful, relevant, and (hopefully) bug-free code. It's like being a translator between human intention and machine interpretation, except the machine sometimes thinks your variable names are suggestions and your architecture decisions are merely guidelines.

In this comprehensive guide, we'll explore the science and art of crafting effective prompts for AI coding assistants. We'll cover everything from basic prompt structure to advanced techniques, share real-world examples (mostly in C# because someone has to represent), and help you avoid the common pitfalls that lead to AI-generated code that makes senior developers weep.


The Fundamentals: Why Prompt Engineering Matters

The Garbage In, Garbage Out Principle

AI coding assistants are incredibly powerful, but they're not mind readers (yet). The quality of their output is directly proportional to the quality and specificity of your input. Consider these two prompts:

Bad Prompt:

write me a class

AI Response:

public class MyClass
{
    public string Property1 { get; set; }
    public int Property2 { get; set; }
}

Well, technically that's a class, but it's about as useful as a chocolate teapot.

Good Prompt:

Create a C# class called CustomerService that handles customer operations. 
Include methods for creating, updating, and retrieving customers. 
Use dependency injection for the repository and logger. 
Add proper error handling and async/await patterns.

AI Response:

public class CustomerService : ICustomerService
{
    private readonly ICustomerRepository _customerRepository;
    private readonly ILogger<CustomerService> _logger;

    public CustomerService(ICustomerRepository customerRepository, ILogger<CustomerService> logger)
    {
        _customerRepository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<Customer> CreateCustomerAsync(CreateCustomerRequest request)
    {
        try
        {
            _logger.LogInformation("Creating customer with email: {Email}", request.Email);
            
            var customer = new Customer
            {
                Name = request.Name,
                Email = request.Email,
                CreatedAt = DateTime.UtcNow
            };

            var result = await _customerRepository.AddAsync(customer);
            _logger.LogInformation("Successfully created customer with ID: {CustomerId}", result.Id);
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to create customer with email: {Email}", request.Email);
            throw;
        }
    }

    public async Task<Customer> UpdateCustomerAsync(int id, UpdateCustomerRequest request)
    {
        try
        {
            var existingCustomer = await _customerRepository.GetByIdAsync(id);
            if (existingCustomer == null)
            {
                throw new CustomerNotFoundException($"Customer with ID {id} not found");
            }

            existingCustomer.Name = request.Name;
            existingCustomer.Email = request.Email;
            existingCustomer.UpdatedAt = DateTime.UtcNow;

            var result = await _customerRepository.UpdateAsync(existingCustomer);
            _logger.LogInformation("Successfully updated customer with ID: {CustomerId}", id);
            
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to update customer with ID: {CustomerId}", id);
            throw;
        }
    }

    public async Task<Customer> GetCustomerAsync(int id)
    {
        try
        {
            var customer = await _customerRepository.GetByIdAsync(id);
            if (customer == null)
            {
                throw new CustomerNotFoundException($"Customer with ID {id} not found");
            }

            return customer;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to retrieve customer with ID: {CustomerId}", id);
            throw;
        }
    }
}

Much better! The AI understood exactly what you wanted and provided a production-ready implementation.


The Anatomy of a Great Prompt

The CLEAR Framework

I've developed a framework called CLEAR for structuring effective prompts:

  • Context: Provide background information
  • Language: Specify the programming language and framework
  • Expectations: Define what success looks like
  • Architecture: Mention patterns, principles, or constraints
  • Requirements: List specific features and functionality

Let's see this in action:

Context: I'm building a microservice for an e-commerce platform that handles order processing.

Language: C# with .NET 8, using Entity Framework Core and ASP.NET Core Web API.

Expectations: The code should be production-ready, well-tested, and follow SOLID principles.

Architecture: Use clean architecture with separate layers for API, business logic, and data access. Implement the repository pattern and use dependency injection.

Requirements: Create an OrderController that can:
1. Create new orders with validation
2. Update order status
3. Get order details by ID
4. List orders with pagination
5. Include proper error handling and logging
6. Return appropriate HTTP status codes

This prompt provides the AI with everything it needs to generate high-quality, contextually appropriate code.


Language-Specific Prompt Techniques

C# and .NET Specific Prompting

When working with C#, include specific framework details to get better results:

Generic Prompt:

Create a service for handling payments

C#-Optimized Prompt:

Create a C# payment service using .NET 8 that:
- Implements IPaymentService interface
- Uses dependency injection with IServiceCollection
- Handles different payment providers (Credit Card, PayPal, Stripe)
- Uses the Strategy pattern for payment processing
- Includes async/await for external API calls
- Has proper exception handling with custom exceptions
- Uses ILogger for structured logging
- Includes XML documentation comments
- Follows C# naming conventions and nullable reference types

AI Response:

using Microsoft.Extensions.Logging;
using System.ComponentModel.DataAnnotations;

/// <summary>
/// Service for processing payments through various payment providers
/// </summary>
public interface IPaymentService
{
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken cancellationToken = default);
    Task<PaymentResult> RefundPaymentAsync(string transactionId, decimal amount, CancellationToken cancellationToken = default);
}

/// <summary>
/// Payment service implementation supporting multiple payment providers
/// </summary>
public class PaymentService : IPaymentService
{
    private readonly Dictionary<PaymentProvider, IPaymentStrategy> _paymentStrategies;
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(
        IEnumerable<IPaymentStrategy> paymentStrategies,
        ILogger<PaymentService> logger)
    {
        _paymentStrategies = paymentStrategies?.ToDictionary(s => s.Provider, s => s)
            ?? throw new ArgumentNullException(nameof(paymentStrategies));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>
    /// Processes a payment using the specified payment provider
    /// </summary>
    /// <param name="request">Payment request details</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>Payment result with transaction details</returns>
    /// <exception cref="PaymentValidationException">Thrown when payment request is invalid</exception>
    /// <exception cref="PaymentProviderException">Thrown when payment provider is not supported</exception>
    public async Task<PaymentResult> ProcessPaymentAsync(
        PaymentRequest request, 
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);

        _logger.LogInformation("Processing payment for amount {Amount} using provider {Provider}", 
            request.Amount, request.Provider);

        try
        {
            // Validate payment request
            ValidatePaymentRequest(request);

            // Get payment strategy
            if (!_paymentStrategies.TryGetValue(request.Provider, out var strategy))
            {
                throw new PaymentProviderException($"Payment provider {request.Provider} is not supported");
            }

            // Process payment
            var result = await strategy.ProcessPaymentAsync(request, cancellationToken);

            _logger.LogInformation("Payment processed successfully. Transaction ID: {TransactionId}", 
                result.TransactionId);

            return result;
        }
        catch (PaymentValidationException ex)
        {
            _logger.LogWarning(ex, "Payment validation failed for request");
            throw;
        }
        catch (PaymentProviderException ex)
        {
            _logger.LogError(ex, "Payment provider error occurred");
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error during payment processing");
            throw new PaymentProcessingException("An unexpected error occurred during payment processing", ex);
        }
    }

    /// <summary>
    /// Processes a refund for a previous transaction
    /// </summary>
    /// <param name="transactionId">Original transaction ID</param>
    /// <param name="amount">Refund amount</param>
    /// <param name="cancellationToken">Cancellation token</param>
    /// <returns>Refund result</returns>
    public async Task<PaymentResult> RefundPaymentAsync(
        string transactionId, 
        decimal amount, 
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(transactionId))
            throw new ArgumentException("Transaction ID cannot be null or empty", nameof(transactionId));

        if (amount <= 0)
            throw new ArgumentException("Refund amount must be greater than zero", nameof(amount));

        _logger.LogInformation("Processing refund for transaction {TransactionId}, amount {Amount}", 
            transactionId, amount);

        try
        {
            // For refunds, we need to determine which provider was used for the original transaction
            // This would typically involve looking up the transaction in your database
            var originalTransaction = await GetOriginalTransactionAsync(transactionId, cancellationToken);
            
            if (!_paymentStrategies.TryGetValue(originalTransaction.Provider, out var strategy))
            {
                throw new PaymentProviderException($"Payment provider {originalTransaction.Provider} is not supported");
            }

            var refundRequest = new RefundRequest
            {
                OriginalTransactionId = transactionId,
                Amount = amount,
                Reason = "Customer requested refund"
            };

            var result = await strategy.ProcessRefundAsync(refundRequest, cancellationToken);

            _logger.LogInformation("Refund processed successfully. Refund Transaction ID: {RefundTransactionId}", 
                result.TransactionId);

            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing refund for transaction {TransactionId}", transactionId);
            throw;
        }
    }

    private static void ValidatePaymentRequest(PaymentRequest request)
    {
        var validationResults = new List<ValidationResult>();
        var validationContext = new ValidationContext(request);

        if (!Validator.TryValidateObject(request, validationContext, validationResults, true))
        {
            var errors = string.Join(", ", validationResults.Select(vr => vr.ErrorMessage));
            throw new PaymentValidationException($"Payment request validation failed: {errors}");
        }

        if (request.Amount <= 0)
        {
            throw new PaymentValidationException("Payment amount must be greater than zero");
        }
    }

    private async Task<Transaction> GetOriginalTransactionAsync(string transactionId, CancellationToken cancellationToken)
    {
        // This would typically query your database to get the original transaction details
        // For now, we'll throw a not implemented exception as this requires database integration
        throw new NotImplementedException("Transaction lookup requires database integration");
    }
}

/// <summary>
/// Strategy interface for different payment providers
/// </summary>
public interface IPaymentStrategy
{
    PaymentProvider Provider { get; }
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken cancellationToken = default);
    Task<PaymentResult> ProcessRefundAsync(RefundRequest request, CancellationToken cancellationToken = default);
}

/// <summary>
/// Stripe payment strategy implementation
/// </summary>
public class StripePaymentStrategy : IPaymentStrategy
{
    private readonly ILogger<StripePaymentStrategy> _logger;
    private readonly StripeSettings _settings;

    public PaymentProvider Provider => PaymentProvider.Stripe;

    public StripePaymentStrategy(ILogger<StripePaymentStrategy> logger, StripeSettings settings)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _settings = settings ?? throw new ArgumentNullException(nameof(settings));
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request, CancellationToken cancellationToken = default)
    {
        _logger.LogDebug("Processing Stripe payment for amount {Amount}", request.Amount);

        try
        {
            // Stripe API integration would go here
            // For demonstration purposes, we'll simulate the call
            await Task.Delay(100, cancellationToken); // Simulate API call

            return new PaymentResult
            {
                Success = true,
                TransactionId = $"stripe_{Guid.NewGuid()}",
                Amount = request.Amount,
                Provider = PaymentProvider.Stripe,
                ProcessedAt = DateTime.UtcNow
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Stripe payment processing failed");
            throw new PaymentProviderException("Stripe payment processing failed", ex);
        }
    }

    public async Task<PaymentResult> ProcessRefundAsync(RefundRequest request, CancellationToken cancellationToken = default)
    {
        _logger.LogDebug("Processing Stripe refund for transaction {TransactionId}", request.OriginalTransactionId);

        try
        {
            // Stripe refund API integration would go here
            await Task.Delay(100, cancellationToken); // Simulate API call

            return new PaymentResult
            {
                Success = true,
                TransactionId = $"stripe_refund_{Guid.NewGuid()}",
                Amount = request.Amount,
                Provider = PaymentProvider.Stripe,
                ProcessedAt = DateTime.UtcNow
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Stripe refund processing failed");
            throw new PaymentProviderException("Stripe refund processing failed", ex);
        }
    }
}

// Supporting classes and enums
public enum PaymentProvider
{
    CreditCard,
    PayPal,
    Stripe
}

public class PaymentRequest
{
    [Required]
    public decimal Amount { get; set; }
    
    [Required]
    public PaymentProvider Provider { get; set; }
    
    [Required]
    public string Currency { get; set; } = "USD";
    
    [Required]
    public PaymentDetails PaymentDetails { get; set; } = new();
}

public class PaymentDetails
{
    public string? CardNumber { get; set; }
    public string? ExpiryMonth { get; set; }
    public string? ExpiryYear { get; set; }
    public string? CVV { get; set; }
    public string? PayPalEmail { get; set; }
    public string? StripeToken { get; set; }
}

public class RefundRequest
{
    [Required]
    public string OriginalTransactionId { get; set; } = string.Empty;
    
    [Required]
    public decimal Amount { get; set; }
    
    public string? Reason { get; set; }
}

public class PaymentResult
{
    public bool Success { get; set; }
    public string TransactionId { get; set; } = string.Empty;
    public decimal Amount { get; set; }
    public PaymentProvider Provider { get; set; }
    public DateTime ProcessedAt { get; set; }
    public string? ErrorMessage { get; set; }
}

public class Transaction
{
    public string Id { get; set; } = string.Empty;
    public PaymentProvider Provider { get; set; }
    public decimal Amount { get; set; }
    public DateTime ProcessedAt { get; set; }
}

public class StripeSettings
{
    public string ApiKey { get; set; } = string.Empty;
    public string WebhookSecret { get; set; } = string.Empty;
}

// Custom exceptions
public class PaymentValidationException : Exception
{
    public PaymentValidationException(string message) : base(message) { }
    public PaymentValidationException(string message, Exception innerException) : base(message, innerException) { }
}

public class PaymentProviderException : Exception
{
    public PaymentProviderException(string message) : base(message) { }
    public PaymentProviderException(string message, Exception innerException) : base(message, innerException) { }
}

public class PaymentProcessingException : Exception
{
    public PaymentProcessingException(string message) : base(message) { }
    public PaymentProcessingException(string message, Exception innerException) : base(message, innerException) { }
}

Now that's what I call a comprehensive response! The AI understood all the C#-specific requirements and delivered production-ready code.


Advanced Prompting Techniques

1. The Iterative Refinement Method

Don't try to get perfect code in one shot. Use iterative prompting to refine and improve:

Round 1:

Create a C# class for handling user authentication

Round 2:

Enhance the authentication class to include:
- JWT token generation and validation
- Password hashing with salt
- Account lockout after failed attempts
- Integration with ASP.NET Core Identity

Round 3:

Add the following security features to the authentication service:
- Two-factor authentication support
- Rate limiting for login attempts
- Audit logging for security events
- Support for external providers (Google, Microsoft)

2. The Example-Driven Approach

Provide examples of what you want to see:

Create a C# repository pattern implementation similar to this structure:

public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    // ... more methods
}

But include these specific features:
- Generic constraint for entities with Id property
- Soft delete support with IsDeleted flag
- Auditing with CreatedAt/UpdatedAt timestamps
- Specification pattern for complex queries
- Unit of work integration
- Connection string configuration

AI Response:

/// <summary>
/// Generic repository interface with auditing and soft delete support
/// </summary>
/// <typeparam name="T">Entity type that implements IAuditableEntity</typeparam>
public interface IRepository<T> where T : class, IAuditableEntity
{
    Task<T?> GetByIdAsync(int id, bool includeDeleted = false);
    Task<IEnumerable<T>> GetAllAsync(bool includeDeleted = false);
    Task<IEnumerable<T>> FindAsync(ISpecification<T> specification);
    Task<T> AddAsync(T entity);
    Task<T> UpdateAsync(T entity);
    Task DeleteAsync(int id, bool hardDelete = false);
    Task<bool> ExistsAsync(int id);
    Task<int> CountAsync(ISpecification<T>? specification = null);
}

/// <summary>
/// Base interface for auditable entities
/// </summary>
public interface IAuditableEntity
{
    int Id { get; set; }
    DateTime CreatedAt { get; set; }
    DateTime? UpdatedAt { get; set; }
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
}

/// <summary>
/// Specification pattern interface for complex queries
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
    Expression<Func<T, object>>? OrderBy { get; }
    Expression<Func<T, object>>? OrderByDescending { get; }
    int Skip { get; }
    int Take { get; }
    bool IsPagingEnabled { get; }
}

/// <summary>
/// Generic repository implementation with Entity Framework Core
/// </summary>
/// <typeparam name="T">Entity type that implements IAuditableEntity</typeparam>
public class Repository<T> : IRepository<T> where T : class, IAuditableEntity
{
    protected readonly DbContext _context;
    protected readonly DbSet<T> _dbSet;
    private readonly ILogger<Repository<T>> _logger;

    public Repository(DbContext context, ILogger<Repository<T>> logger)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _dbSet = _context.Set<T>();
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public virtual async Task<T?> GetByIdAsync(int id, bool includeDeleted = false)
    {
        _logger.LogDebug("Retrieving entity {EntityType} with ID {Id}", typeof(T).Name, id);

        var query = _dbSet.AsQueryable();
        
        if (!includeDeleted)
        {
            query = query.Where(e => !e.IsDeleted);
        }

        return await query.FirstOrDefaultAsync(e => e.Id == id);
    }

    public virtual async Task<IEnumerable<T>> GetAllAsync(bool includeDeleted = false)
    {
        _logger.LogDebug("Retrieving all entities of type {EntityType}", typeof(T).Name);

        var query = _dbSet.AsQueryable();
        
        if (!includeDeleted)
        {
            query = query.Where(e => !e.IsDeleted);
        }

        return await query.ToListAsync();
    }

    public virtual async Task<IEnumerable<T>> FindAsync(ISpecification<T> specification)
    {
        _logger.LogDebug("Finding entities of type {EntityType} with specification", typeof(T).Name);

        var query = ApplySpecification(specification);
        return await query.ToListAsync();
    }

    public virtual async Task<T> AddAsync(T entity)
    {
        ArgumentNullException.ThrowIfNull(entity);

        _logger.LogDebug("Adding new entity of type {EntityType}", typeof(T).Name);

        entity.CreatedAt = DateTime.UtcNow;
        entity.IsDeleted = false;

        _dbSet.Add(entity);
        await _context.SaveChangesAsync();

        _logger.LogInformation("Successfully added entity {EntityType} with ID {Id}", typeof(T).Name, entity.Id);
        return entity;
    }

    public virtual async Task<T> UpdateAsync(T entity)
    {
        ArgumentNullException.ThrowIfNull(entity);

        _logger.LogDebug("Updating entity {EntityType} with ID {Id}", typeof(T).Name, entity.Id);

        entity.UpdatedAt = DateTime.UtcNow;
        
        _context.Entry(entity).State = EntityState.Modified;
        await _context.SaveChangesAsync();

        _logger.LogInformation("Successfully updated entity {EntityType} with ID {Id}", typeof(T).Name, entity.Id);
        return entity;
    }

    public virtual async Task DeleteAsync(int id, bool hardDelete = false)
    {
        _logger.LogDebug("Deleting entity {EntityType} with ID {Id}, Hard Delete: {HardDelete}", 
            typeof(T).Name, id, hardDelete);

        var entity = await GetByIdAsync(id, includeDeleted: true);
        if (entity == null)
        {
            throw new EntityNotFoundException($"Entity {typeof(T).Name} with ID {id} not found");
        }

        if (hardDelete)
        {
            _dbSet.Remove(entity);
        }
        else
        {
            entity.IsDeleted = true;
            entity.DeletedAt = DateTime.UtcNow;
            _context.Entry(entity).State = EntityState.Modified;
        }

        await _context.SaveChangesAsync();

        _logger.LogInformation("Successfully deleted entity {EntityType} with ID {Id}, Hard Delete: {HardDelete}", 
            typeof(T).Name, id, hardDelete);
    }

    public virtual async Task<bool> ExistsAsync(int id)
    {
        return await _dbSet.AnyAsync(e => e.Id == id && !e.IsDeleted);
    }

    public virtual async Task<int> CountAsync(ISpecification<T>? specification = null)
    {
        var query = _dbSet.Where(e => !e.IsDeleted);
        
        if (specification != null)
        {
            query = query.Where(specification.Criteria);
        }

        return await query.CountAsync();
    }

    private IQueryable<T> ApplySpecification(ISpecification<T> specification)
    {
        var query = _dbSet.AsQueryable();

        // Apply criteria
        if (specification.Criteria != null)
        {
            query = query.Where(specification.Criteria);
        }

        // Apply includes
        query = specification.Includes
            .Aggregate(query, (current, include) => current.Include(include));

        // Apply ordering
        if (specification.OrderBy != null)
        {
            query = query.OrderBy(specification.OrderBy);
        }
        else if (specification.OrderByDescending != null)
        {
            query = query.OrderByDescending(specification.OrderByDescending);
        }

        // Apply paging
        if (specification.IsPagingEnabled)
        {
            query = query.Skip(specification.Skip).Take(specification.Take);
        }

        // Exclude soft deleted entities by default
        if (!specification.Criteria.ToString().Contains(nameof(IAuditableEntity.IsDeleted)))
        {
            query = query.Where(e => !e.IsDeleted);
        }

        return query;
    }
}

/// <summary>
/// Base specification class for common query patterns
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public abstract class BaseSpecification<T> : ISpecification<T>
{
    protected BaseSpecification(Expression<Func<T, bool>>? criteria = null)
    {
        Criteria = criteria;
    }

    public Expression<Func<T, bool>>? Criteria { get; }
    public List<Expression<Func<T, object>>> Includes { get; } = new();
    public Expression<Func<T, object>>? OrderBy { get; private set; }
    public Expression<Func<T, object>>? OrderByDescending { get; private set; }
    public int Skip { get; private set; }
    public int Take { get; private set; }
    public bool IsPagingEnabled { get; private set; }

    protected void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }

    protected void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
    {
        OrderBy = orderByExpression;
    }

    protected void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescExpression)
    {
        OrderByDescending = orderByDescExpression;
    }

    protected void ApplyPaging(int skip, int take)
    {
        Skip = skip;
        Take = take;
        IsPagingEnabled = true;
    }
}

/// <summary>
/// Unit of Work interface for managing repositories and transactions
/// </summary>
public interface IUnitOfWork : IDisposable
{
    IRepository<T> Repository<T>() where T : class, IAuditableEntity;
    Task<int> SaveChangesAsync();
    Task BeginTransactionAsync();
    Task CommitTransactionAsync();
    Task RollbackTransactionAsync();
}

/// <summary>
/// Unit of Work implementation
/// </summary>
public class UnitOfWork : IUnitOfWork
{
    private readonly DbContext _context;
    private readonly IServiceProvider _serviceProvider;
    private readonly Dictionary<Type, object> _repositories = new();
    private IDbContextTransaction? _transaction;
    private bool _disposed;

    public UnitOfWork(DbContext context, IServiceProvider serviceProvider)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
    }

    public IRepository<T> Repository<T>() where T : class, IAuditableEntity
    {
        var type = typeof(T);
        
        if (_repositories.TryGetValue(type, out var existingRepository))
        {
            return (IRepository<T>)existingRepository;
        }

        var repository = _serviceProvider.GetRequiredService<IRepository<T>>();
        _repositories[type] = repository;
        
        return repository;
    }

    public async Task<int> SaveChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }

    public async Task BeginTransactionAsync()
    {
        _transaction = await _context.Database.BeginTransactionAsync();
    }

    public async Task CommitTransactionAsync()
    {
        if (_transaction != null)
        {
            await _transaction.CommitAsync();
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public async Task RollbackTransactionAsync()
    {
        if (_transaction != null)
        {
            await _transaction.RollbackAsync();
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public void Dispose()
    {
        if (!_disposed)
        {
            _transaction?.Dispose();
            _context.Dispose();
            _disposed = true;
        }
    }
}

/// <summary>
/// Custom exception for entity not found scenarios
/// </summary>
public class EntityNotFoundException : Exception
{
    public EntityNotFoundException(string message) : base(message) { }
    public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) { }
}

// Example entity implementation
public class Customer : IAuditableEntity
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

// Example specification implementation
public class CustomersByEmailSpecification : BaseSpecification<Customer>
{
    public CustomersByEmailSpecification(string emailDomain) : base(c => c.Email.Contains(emailDomain))
    {
        ApplyOrderBy(c => c.Name);
    }
}

// Dependency injection setup
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddRepositoryPattern(this IServiceCollection services, string connectionString)
    {
        services.AddDbContext<DbContext>(options =>
            options.UseSqlServer(connectionString));

        services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
        services.AddScoped<IUnitOfWork, UnitOfWork>();

        return services;
    }
}

3. The Context-Setting Technique

Help AI understand your specific situation:

I'm working on a legacy .NET Framework 4.8 application that needs to be gradually migrated to .NET 8. 
The application currently uses:
- Web Forms for UI
- ADO.NET for data access
- Session state for user management
- SOAP web services for integration

Create a migration strategy and show me how to implement a new API controller in .NET 8 that can:
1. Accept requests from the legacy Web Forms application
2. Use modern async/await patterns
3. Implement proper logging and error handling
4. Maintain backward compatibility with existing data structures
5. Gradually replace the SOAP services

The new controller should handle customer data operations and serve as a bridge between old and new systems.

Common Prompt Engineering Mistakes

Mistake 1: Being Too Vague

Bad:

Make this code better

Good:

Refactor this C# method to:
- Follow single responsibility principle
- Add proper error handling
- Use async/await pattern
- Add XML documentation
- Improve variable naming
- Add unit test examples

Mistake 2: Overloading with Requirements

Bad:

Create a complete e-commerce system with user authentication, payment processing, inventory management, order tracking, email notifications, admin dashboard, reporting, analytics, multi-language support, mobile app API, third-party integrations, caching, logging, monitoring, CI/CD pipeline, Docker containers, Kubernetes deployment, and microservices architecture.

Good:

Create the foundation for an e-commerce system starting with:
1. A Product model with basic properties
2. A ProductService with CRUD operations
3. A ProductController with REST endpoints
4. Basic validation and error handling
5. Integration with Entity Framework Core

We'll build additional features incrementally.

Mistake 3: Ignoring Context

Bad:

Write a function to sort a list

Good:

Write a C# extension method that sorts a List<Customer> by multiple criteria:
- Primary sort: LastName (ascending)
- Secondary sort: FirstName (ascending)  
- Tertiary sort: DateCreated (descending)

The method should handle null values gracefully and be performant for lists up to 10,000 items.

Domain-Specific Prompting Strategies

Web API Development

Create an ASP.NET Core Web API controller for managing products in an e-commerce system with these requirements:

Technical specs:
- .NET 8 with minimal APIs pattern
- OpenAPI/Swagger documentation
- FluentValidation for request validation
- AutoMapper for DTO mapping
- Serilog for structured logging
- Problem Details for error responses

Endpoints needed:
- GET /api/products (with pagination, filtering, sorting)
- GET /api/products/{id}
- POST /api/products
- PUT /api/products/{id}
- DELETE /api/products/{id}
- GET /api/products/search?q={query}

Business rules:
- Products must have unique SKUs
- Price must be greater than 0
- Category is required
- Soft delete for products
- Audit trail for all changes
- Cache frequently accessed products

Include comprehensive error handling, request/response DTOs, and example requests.

Database Design and Entity Framework

Design a C# Entity Framework Core model for a library management system with these entities:

Entities:
- Book (Title, ISBN, Authors, Publisher, PublicationDate, Genre, etc.)
- Author (Name, Biography, BirthDate, Nationality)
- Member (Name, Email, Phone, Address, MembershipDate)
- Loan (Book, Member, LoanDate, DueDate, ReturnDate, FineAmount)
- Reservation (Book, Member, ReservationDate, ExpiryDate, Status)

Requirements:
- Many-to-many relationship between Books and Authors
- One-to-many relationship between Members and Loans
- Implement audit fields (CreatedAt, UpdatedAt, CreatedBy, UpdatedBy)
- Use appropriate indexes for performance
- Include data annotations for validation
- Configure relationships using Fluent API
- Add seed data for testing
- Include repository pattern implementation

Microservices Architecture

Design a C# microservice for handling user notifications with these specifications:

Architecture:
- ASP.NET Core Web API
- Event-driven communication using MediatR
- Message queuing with RabbitMQ or Azure Service Bus
- Redis for caching
- Entity Framework Core with PostgreSQL
- Docker containerization

Features:
- Send notifications via email, SMS, and push notifications
- Template management for notification content
- User preferences for notification types
- Delivery status tracking
- Retry mechanism for failed deliveries
- Rate limiting to prevent spam
- Analytics for notification performance

Integration points:
- Consume user events from User Service
- Consume order events from Order Service
- Expose REST API for manual notifications
- Provide webhook endpoints for delivery status updates

Include configuration, dependency injection setup, and health checks.

Testing and Validation Prompts

Unit Testing

Generate comprehensive unit tests for this C# service class using NUnit and Moq:

[Include your service class code here]

Test requirements:
- Test all public methods with various input scenarios
- Mock all dependencies appropriately
- Test edge cases and error conditions
- Use parameterized tests where appropriate
- Include async test patterns for async methods
- Test exception handling scenarios
- Achieve 90%+ code coverage
- Follow AAA (Arrange, Act, Assert) pattern
- Use descriptive test method names that explain the scenario

Include setup/teardown methods and any test data builders needed.

Integration Testing

Create integration tests for an ASP.NET Core Web API using WebApplicationFactory:

API to test: Product Management API with endpoints for CRUD operations
Database: Use in-memory database for testing
Authentication: JWT bearer tokens

Test scenarios:
- Successful product creation with valid data
- Product creation with invalid data (validation errors)
- Retrieving products with pagination
- Updating existing products
- Deleting products (soft delete)
- Unauthorized access attempts
- Performance testing for bulk operations

Include test data setup, authentication helpers, and assertion methods.

Performance and Optimization Prompts

Performance Analysis

Analyze this C# code for performance bottlenecks and provide optimized versions:

[Include your code here]

Focus areas:
- Database query optimization
- Memory allocation reduction
- Async/await usage
- Caching opportunities
- LINQ query efficiency
- String manipulation improvements
- Collection usage optimization

For each optimization:
- Explain the performance issue
- Show the improved code
- Estimate the performance gain
- Mention any trade-offs
- Include benchmarking code using BenchmarkDotNet

Caching Strategies

Implement a comprehensive caching strategy for a C# e-commerce application:

Scenarios to cache:
- Product catalog data (frequently read, rarely updated)
- User session data (fast access required)
- Shopping cart contents (temporary, user-specific)
- Search results (expensive to compute)
- Configuration settings (rarely changed)

Requirements:
- Use IMemoryCache for local caching
- Use IDistributedCache (Redis) for shared caching
- Implement cache-aside pattern
- Add cache invalidation strategies
- Include cache performance monitoring
- Handle cache failures gracefully
- Configure appropriate expiration policies

Show examples for each caching scenario with proper error handling and monitoring.

Security-Focused Prompts

Authentication and Authorization

Implement a robust authentication and authorization system for an ASP.NET Core API:

Requirements:
- JWT token-based authentication
- Role-based and claims-based authorization
- Password policies and validation
- Account lockout after failed attempts
- Two-factor authentication support
- Password reset functionality
- Audit logging for security events
- Rate limiting for auth endpoints
- Secure token storage and refresh

Security standards:
- Follow OWASP guidelines
- Implement CSRF protection
- Use secure headers
- Validate all inputs
- Implement proper session management
- Include security unit tests

Provide complete implementation with configuration, middleware setup, and usage examples.

Data Protection

Create a C# data protection system for handling sensitive customer information:

Data types to protect:
- Credit card numbers
- Social security numbers
- Personal identification documents
- Medical records
- Financial information

Requirements:
- Encryption at rest and in transit
- Field-level encryption for sensitive data
- Key rotation capabilities
- Audit trail for data access
- GDPR compliance features (right to be forgotten)
- Data masking for non-production environments
- Secure key management
- Integration with Azure Key Vault or similar

Include implementation examples, configuration, and compliance documentation.

Advanced Prompt Patterns

The Teaching Pattern

I'm learning about Clean Architecture in .NET. Can you explain the concept and then show me how to implement it step by step?

Start with:
1. Explanation of Clean Architecture principles
2. Show the folder structure for a .NET solution
3. Implement the core entities
4. Add the application layer with use cases
5. Implement the infrastructure layer
6. Create the presentation layer (API)
7. Show how dependency injection wires everything together
8. Explain the benefits and trade-offs

Use a simple e-commerce domain (products, orders, customers) for the examples.

The Comparison Pattern

Compare and contrast these three approaches for handling database transactions in C#:

1. Entity Framework Core transactions
2. ADO.NET transactions  
3. Distributed transactions with TransactionScope

For each approach, show:
- Implementation example
- Performance characteristics
- When to use each approach
- Pros and cons
- Error handling patterns
- Testing strategies

Include scenarios where each approach would be most appropriate.

The Migration Pattern

I need to migrate this legacy C# code from .NET Framework 4.8 to .NET 8:

[Include legacy code here]

Show me:
1. What needs to be changed and why
2. Step-by-step migration process
3. Modern equivalents for deprecated APIs
4. Performance improvements in the new version
5. New features I can take advantage of
6. Testing strategy to ensure compatibility
7. Potential breaking changes to watch for

Provide both the migrated code and migration guidance.

Measuring Prompt Effectiveness

Key Metrics to Track

  1. Code Quality: Does the AI generate production-ready code?
  2. Relevance: How well does the output match your requirements?
  3. Completeness: Are all requested features implemented?
  4. Best Practices: Does the code follow established patterns?
  5. Error Rate: How often do you need to fix AI-generated code?

Iterative Improvement

Keep a prompt library of your most effective prompts:

public class PromptLibrary
{
    public static readonly Dictionary<string, string> CommonPrompts = new()
    {
        ["service-class"] = @"Create a C# service class for {domain} with these requirements:
            - Dependency injection with interface
            - Async/await patterns for I/O operations
            - Proper error handling and logging
            - XML documentation comments
            - Unit test examples
            - Include these methods: {methods}",
            
        ["api-controller"] = @"Create an ASP.NET Core controller for {entity} with:
            - Full CRUD operations
            - Proper HTTP status codes
            - Request/response DTOs
            - Validation attributes
            - OpenAPI documentation
            - Error handling middleware integration",
            
        ["repository-pattern"] = @"Implement repository pattern for {entity} using:
            - Generic interface with {entity}-specific methods
            - Entity Framework Core implementation
            - Async methods throughout
            - Specification pattern for complex queries
            - Unit of work integration
            - Proper exception handling"
    };
}

The Future of Prompt Engineering

Emerging Patterns

As AI models become more sophisticated, prompt engineering is evolving:

  1. Context-Aware Prompting: AI models that remember previous conversations
  2. Code-First Prompting: Starting with existing code and asking for modifications
  3. Multi-Modal Prompting: Including diagrams, screenshots, and other media
  4. Collaborative Prompting: AI systems that ask clarifying questions

Best Practices for the Future

// Future prompt might look like this
public class FuturePrompt
{
    public string Context { get; set; } = "Building microservices for e-commerce";
    public string[] PreviousConversation { get; set; } = new[] { /* conversation history */ };
    public CodeContext ExistingCode { get; set; } = new() { /* current codebase */ };
    public string Request { get; set; } = "Add caching to the product service";
    public QualityRequirements Quality { get; set; } = new() 
    { 
        TestCoverage = 90,
        PerformanceRequirements = "Sub-100ms response time",
        SecurityCompliance = "OWASP Top 10"
    };
}

Conclusion: Mastering the Art of AI Communication

Prompt engineering for developers is both an art and a science. It requires understanding not just what you want to build, but how to communicate that vision clearly to an artificial intelligence that's incredibly powerful but lacks the context and intuition that come naturally to humans.

The key takeaways for effective prompt engineering:

  1. Be Specific: The more context and detail you provide, the better the results
  2. Iterate and Refine: Don't expect perfection on the first try
  3. Use Examples: Show the AI what good looks like in your domain
  4. Think in Layers: Build complex solutions incrementally
  5. Validate Everything: AI is a powerful assistant, not a replacement for human judgment

As AI coding assistants continue to evolve, the developers who master prompt engineering will have a significant advantage. They'll be able to leverage AI to handle routine tasks, explore new patterns, and prototype solutions faster than ever before.

But remember: the goal isn't to replace human creativity and problem-solving—it's to augment it. The best results come from the collaboration between human insight and AI capability.

So go forth and prompt with purpose. Your AI coding assistant is waiting to help you build amazing things—you just need to know how to ask.

Happy prompting! 🤖💻✨