Claude Code Plugins

Community-maintained marketplace

Feedback

dotnet-best-practices

@DoubleslashSE/claude-workflows
0
0

.NET development best practices including SOLID, KISS, YAGNI, DRY principles, design patterns, and anti-patterns to avoid.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name dotnet-best-practices
description .NET development best practices including SOLID, KISS, YAGNI, DRY principles, design patterns, and anti-patterns to avoid.

.NET Best Practices

Fundamental principles and patterns for writing maintainable, scalable, and clean .NET code.

Core Principles

┌─────────────────────────────────────────────────────────────────┐
│                     GUIDING PRINCIPLES                          │
│                                                                 │
│   SOLID ─── Foundation for object-oriented design               │
│   DRY   ─── Don't Repeat Yourself                               │
│   KISS  ─── Keep It Simple, Stupid                              │
│   YAGNI ─── You Aren't Gonna Need It                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

SOLID Principles

S - Single Responsibility Principle

A class should have only one reason to change.

// BAD - Multiple responsibilities
public class UserService
{
    public void CreateUser(User user) { /* ... */ }
    public void SendWelcomeEmail(User user) { /* ... */ }
    public void GenerateReport(User user) { /* ... */ }
    public void LogUserActivity(User user) { /* ... */ }
}

// GOOD - Single responsibility per class
public class UserService
{
    private readonly IEmailService _emailService;
    private readonly IUserRepository _repository;

    public async Task<User> CreateUserAsync(CreateUserRequest request)
    {
        var user = User.Create(request.Email, request.Name);
        await _repository.AddAsync(user);
        await _emailService.SendWelcomeEmailAsync(user);
        return user;
    }
}

public class EmailService : IEmailService
{
    public Task SendWelcomeEmailAsync(User user) { /* ... */ }
}

public class UserReportGenerator : IReportGenerator<User>
{
    public Task<Report> GenerateAsync(User user) { /* ... */ }
}

O - Open/Closed Principle

Software entities should be open for extension, but closed for modification.

// BAD - Must modify class to add new discount types
public class DiscountCalculator
{
    public decimal Calculate(Order order, string discountType)
    {
        return discountType switch
        {
            "percentage" => order.Total * 0.1m,
            "fixed" => order.Total - 10m,
            "loyalty" => order.Total * 0.15m,
            // Must add new cases here...
            _ => order.Total
        };
    }
}

// GOOD - Open for extension via new implementations
public interface IDiscountStrategy
{
    decimal Apply(Order order);
}

public class PercentageDiscount : IDiscountStrategy
{
    private readonly decimal _percentage;
    public PercentageDiscount(decimal percentage) => _percentage = percentage;
    public decimal Apply(Order order) => order.Total * _percentage;
}

public class FixedDiscount : IDiscountStrategy
{
    private readonly decimal _amount;
    public FixedDiscount(decimal amount) => _amount = amount;
    public decimal Apply(Order order) => Math.Max(0, order.Total - _amount);
}

public class LoyaltyDiscount : IDiscountStrategy
{
    public decimal Apply(Order order) => order.Total * 0.15m;
}

// New discounts added without modifying existing code
public class BulkDiscount : IDiscountStrategy
{
    public decimal Apply(Order order) =>
        order.Items.Count > 10 ? order.Total * 0.2m : order.Total;
}

L - Liskov Substitution Principle

Subtypes must be substitutable for their base types.

// BAD - Square breaks Rectangle behavior
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    public override int Width
    {
        set { base.Width = base.Height = value; }
    }
    public override int Height
    {
        set { base.Width = base.Height = value; }
    }
}

// This breaks LSP:
Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
// Expected: Area = 50, Actual: Area = 100

// GOOD - Separate abstractions
public interface IShape
{
    int Area { get; }
}

public class Rectangle : IShape
{
    public int Width { get; }
    public int Height { get; }
    public int Area => Width * Height;

    public Rectangle(int width, int height)
    {
        Width = width;
        Height = height;
    }
}

public class Square : IShape
{
    public int Side { get; }
    public int Area => Side * Side;

    public Square(int side) => Side = side;
}

I - Interface Segregation Principle

Clients should not be forced to depend on interfaces they don't use.

// BAD - Fat interface forces unnecessary implementations
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
    void AttendMeeting();
    void WriteReport();
}

public class Robot : IWorker
{
    public void Work() { /* ... */ }
    public void Eat() => throw new NotSupportedException(); // Forced to implement
    public void Sleep() => throw new NotSupportedException();
    public void AttendMeeting() => throw new NotSupportedException();
    public void WriteReport() => throw new NotSupportedException();
}

// GOOD - Segregated interfaces
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface IRestable
{
    void Sleep();
}

public class Human : IWorkable, IFeedable, IRestable
{
    public void Work() { /* ... */ }
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
}

public class Robot : IWorkable
{
    public void Work() { /* ... */ }
}

D - Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions.

// BAD - High-level depends on low-level
public class OrderService
{
    private readonly SqlDatabase _database = new SqlDatabase();
    private readonly SmtpEmailSender _emailSender = new SmtpEmailSender();

    public void ProcessOrder(Order order)
    {
        _database.Save(order);
        _emailSender.Send(order.CustomerEmail, "Order confirmed");
    }
}

// GOOD - Both depend on abstractions
public interface IOrderRepository
{
    Task SaveAsync(Order order);
}

public interface IEmailSender
{
    Task SendAsync(string to, string message);
}

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IEmailSender _emailSender;

    public OrderService(IOrderRepository repository, IEmailSender emailSender)
    {
        _repository = repository;
        _emailSender = emailSender;
    }

    public async Task ProcessOrderAsync(Order order)
    {
        await _repository.SaveAsync(order);
        await _emailSender.SendAsync(order.CustomerEmail, "Order confirmed");
    }
}

DRY - Don't Repeat Yourself

Every piece of knowledge should have a single, unambiguous representation.

// BAD - Repeated validation logic
public class UserController
{
    public IActionResult Create(UserRequest request)
    {
        if (string.IsNullOrEmpty(request.Email) || !request.Email.Contains("@"))
            return BadRequest("Invalid email");
        // ...
    }

    public IActionResult Update(UserRequest request)
    {
        if (string.IsNullOrEmpty(request.Email) || !request.Email.Contains("@"))
            return BadRequest("Invalid email");
        // ...
    }
}

// GOOD - Single source of truth
public static class EmailValidator
{
    private static readonly Regex EmailRegex = new(
        @"^[^@\s]+@[^@\s]+\.[^@\s]+$",
        RegexOptions.Compiled);

    public static bool IsValid(string email) =>
        !string.IsNullOrWhiteSpace(email) && EmailRegex.IsMatch(email);
}

// Or use FluentValidation
public class UserRequestValidator : AbstractValidator<UserRequest>
{
    public UserRequestValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .WithMessage("Invalid email format");
    }
}

KISS - Keep It Simple, Stupid

Prefer simple solutions over complex ones.

// BAD - Over-engineered
public class StringHelper
{
    public string Reverse(string input)
    {
        if (input == null)
            throw new ArgumentNullException(nameof(input));

        var factory = new StringBuilderFactory();
        var builder = factory.Create(input.Length);
        var strategy = new ReverseIterationStrategy();

        foreach (var index in strategy.GetIndices(input.Length))
        {
            builder.Append(input[index]);
        }

        return builder.ToString();
    }
}

// GOOD - Simple and clear
public static class StringExtensions
{
    public static string Reverse(this string input)
    {
        if (string.IsNullOrEmpty(input))
            return input;

        var chars = input.ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }
}

YAGNI - You Aren't Gonna Need It

Don't add functionality until it's necessary.

// BAD - Building for hypothetical future requirements
public class UserService
{
    private readonly ICache _cache;
    private readonly IMessageQueue _queue;
    private readonly IAnalyticsService _analytics;
    private readonly IAuditLog _auditLog;
    private readonly IFeatureFlags _featureFlags;

    public async Task<User> GetUserAsync(Guid id)
    {
        // Complex caching logic for "future scale"
        // Message queue integration "in case we need it"
        // Analytics tracking "for later"
        // Audit logging "just in case"
        // Feature flags "for flexibility"

        // When all you need is:
        return await _repository.GetByIdAsync(id);
    }
}

// GOOD - Only what's needed now
public class UserService
{
    private readonly IUserRepository _repository;

    public UserService(IUserRepository repository)
    {
        _repository = repository;
    }

    public Task<User?> GetUserAsync(Guid id) =>
        _repository.GetByIdAsync(id);
}

// Add caching WHEN you have performance issues
// Add analytics WHEN you have analytics requirements
// Add audit logging WHEN compliance requires it

Design Patterns

Strategy Pattern

Encapsulate algorithms and make them interchangeable.

// Define strategy interface
public interface IPaymentStrategy
{
    Task<PaymentResult> ProcessAsync(Payment payment);
}

// Concrete strategies
public class CreditCardPayment : IPaymentStrategy
{
    private readonly IPaymentGateway _gateway;

    public CreditCardPayment(IPaymentGateway gateway) => _gateway = gateway;

    public async Task<PaymentResult> ProcessAsync(Payment payment)
    {
        var response = await _gateway.ChargeAsync(payment.Amount, payment.CardToken);
        return new PaymentResult(response.Success, response.TransactionId);
    }
}

public class PayPalPayment : IPaymentStrategy
{
    private readonly IPayPalClient _client;

    public PayPalPayment(IPayPalClient client) => _client = client;

    public async Task<PaymentResult> ProcessAsync(Payment payment)
    {
        var response = await _client.ExecutePaymentAsync(payment.PayPalOrderId);
        return new PaymentResult(response.Status == "COMPLETED", response.Id);
    }
}

public class BankTransferPayment : IPaymentStrategy
{
    public Task<PaymentResult> ProcessAsync(Payment payment)
    {
        // Generate transfer instructions
        return Task.FromResult(new PaymentResult(true, Guid.NewGuid().ToString()));
    }
}

// Context
public class PaymentProcessor
{
    private readonly IEnumerable<IPaymentStrategy> _strategies;

    public PaymentProcessor(IEnumerable<IPaymentStrategy> strategies)
    {
        _strategies = strategies;
    }

    public Task<PaymentResult> ProcessAsync(Payment payment)
    {
        var strategy = _strategies.FirstOrDefault(s =>
            s.GetType().Name.StartsWith(payment.Method.ToString()));

        return strategy?.ProcessAsync(payment)
            ?? throw new NotSupportedException($"Payment method {payment.Method} not supported");
    }
}

Factory Pattern

Encapsulate object creation logic.

// Simple Factory
public interface INotification
{
    Task SendAsync(string recipient, string message);
}

public class EmailNotification : INotification { /* ... */ }
public class SmsNotification : INotification { /* ... */ }
public class PushNotification : INotification { /* ... */ }

public class NotificationFactory
{
    private readonly IServiceProvider _serviceProvider;

    public NotificationFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public INotification Create(NotificationType type) => type switch
    {
        NotificationType.Email => _serviceProvider.GetRequiredService<EmailNotification>(),
        NotificationType.Sms => _serviceProvider.GetRequiredService<SmsNotification>(),
        NotificationType.Push => _serviceProvider.GetRequiredService<PushNotification>(),
        _ => throw new ArgumentException($"Unknown notification type: {type}")
    };
}

// Abstract Factory
public interface IUIFactory
{
    IButton CreateButton();
    ITextBox CreateTextBox();
    IDropdown CreateDropdown();
}

public class MaterialUIFactory : IUIFactory
{
    public IButton CreateButton() => new MaterialButton();
    public ITextBox CreateTextBox() => new MaterialTextBox();
    public IDropdown CreateDropdown() => new MaterialDropdown();
}

public class BootstrapUIFactory : IUIFactory
{
    public IButton CreateButton() => new BootstrapButton();
    public ITextBox CreateTextBox() => new BootstrapTextBox();
    public IDropdown CreateDropdown() => new BootstrapDropdown();
}

Repository Pattern

Abstract data access logic.

// Generic repository interface
public interface IRepository<T> where T : class, IEntity
{
    Task<T?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct = default);
    Task AddAsync(T entity, CancellationToken ct = default);
    void Update(T entity);
    void Delete(T entity);
}

// Specification pattern for complex queries
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? Take { get; }
    int? Skip { get; }
}

public interface IRepository<T> where T : class, IEntity
{
    Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec, CancellationToken ct = default);
    Task<T?> FirstOrDefaultAsync(ISpecification<T> spec, CancellationToken ct = default);
    Task<int> CountAsync(ISpecification<T> spec, CancellationToken ct = default);
}

// Usage
public class ActiveAuctionsSpec : Specification<Auction>
{
    public ActiveAuctionsSpec()
    {
        AddCriteria(a => a.Status == AuctionStatus.Active);
        AddCriteria(a => a.EndsAt > DateTime.UtcNow);
        ApplyOrderBy(a => a.EndsAt);
    }
}

var activeAuctions = await _repository.ListAsync(new ActiveAuctionsSpec());

Builder Pattern

Construct complex objects step by step.

public class EmailBuilder
{
    private string _from = string.Empty;
    private readonly List<string> _to = new();
    private readonly List<string> _cc = new();
    private string _subject = string.Empty;
    private string _body = string.Empty;
    private bool _isHtml;
    private readonly List<Attachment> _attachments = new();

    public EmailBuilder From(string email)
    {
        _from = email;
        return this;
    }

    public EmailBuilder To(string email)
    {
        _to.Add(email);
        return this;
    }

    public EmailBuilder To(IEnumerable<string> emails)
    {
        _to.AddRange(emails);
        return this;
    }

    public EmailBuilder Cc(string email)
    {
        _cc.Add(email);
        return this;
    }

    public EmailBuilder WithSubject(string subject)
    {
        _subject = subject;
        return this;
    }

    public EmailBuilder WithBody(string body, bool isHtml = false)
    {
        _body = body;
        _isHtml = isHtml;
        return this;
    }

    public EmailBuilder WithAttachment(string path, string name)
    {
        _attachments.Add(new Attachment(path, name));
        return this;
    }

    public Email Build()
    {
        if (string.IsNullOrEmpty(_from))
            throw new InvalidOperationException("From address is required");
        if (!_to.Any())
            throw new InvalidOperationException("At least one recipient is required");

        return new Email(_from, _to, _cc, _subject, _body, _isHtml, _attachments);
    }
}

// Usage
var email = new EmailBuilder()
    .From("noreply@example.com")
    .To("user@example.com")
    .Cc("manager@example.com")
    .WithSubject("Order Confirmation")
    .WithBody("<h1>Thank you for your order!</h1>", isHtml: true)
    .WithAttachment("/invoices/12345.pdf", "invoice.pdf")
    .Build();

Decorator Pattern

Add behavior to objects dynamically.

public interface IMessageSender
{
    Task SendAsync(Message message);
}

public class EmailSender : IMessageSender
{
    public Task SendAsync(Message message)
    {
        // Send email
        return Task.CompletedTask;
    }
}

// Decorators
public class LoggingMessageSender : IMessageSender
{
    private readonly IMessageSender _inner;
    private readonly ILogger<LoggingMessageSender> _logger;

    public LoggingMessageSender(IMessageSender inner, ILogger<LoggingMessageSender> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task SendAsync(Message message)
    {
        _logger.LogInformation("Sending message to {Recipient}", message.Recipient);
        await _inner.SendAsync(message);
        _logger.LogInformation("Message sent successfully");
    }
}

public class RetryMessageSender : IMessageSender
{
    private readonly IMessageSender _inner;
    private readonly int _maxRetries;

    public RetryMessageSender(IMessageSender inner, int maxRetries = 3)
    {
        _inner = inner;
        _maxRetries = maxRetries;
    }

    public async Task SendAsync(Message message)
    {
        for (int i = 0; i < _maxRetries; i++)
        {
            try
            {
                await _inner.SendAsync(message);
                return;
            }
            catch when (i < _maxRetries - 1)
            {
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
            }
        }
    }
}

// Registration with decoration
services.AddScoped<EmailSender>();
services.AddScoped<IMessageSender>(sp =>
    new LoggingMessageSender(
        new RetryMessageSender(
            sp.GetRequiredService<EmailSender>()),
        sp.GetRequiredService<ILogger<LoggingMessageSender>>()));

Observer Pattern (Events)

Define a subscription mechanism for notifications.

// Using domain events
public interface IDomainEvent
{
    DateTime OccurredOn { get; }
}

public record OrderPlacedEvent(Order Order) : IDomainEvent
{
    public DateTime OccurredOn { get; } = DateTime.UtcNow;
}

public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent
{
    Task HandleAsync(TEvent domainEvent, CancellationToken ct = default);
}

public class SendOrderConfirmationHandler : IDomainEventHandler<OrderPlacedEvent>
{
    private readonly IEmailService _emailService;

    public SendOrderConfirmationHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }

    public Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken ct)
    {
        return _emailService.SendOrderConfirmationAsync(domainEvent.Order, ct);
    }
}

public class UpdateInventoryHandler : IDomainEventHandler<OrderPlacedEvent>
{
    private readonly IInventoryService _inventoryService;

    public UpdateInventoryHandler(IInventoryService inventoryService)
    {
        _inventoryService = inventoryService;
    }

    public Task HandleAsync(OrderPlacedEvent domainEvent, CancellationToken ct)
    {
        return _inventoryService.ReserveItemsAsync(domainEvent.Order.Items, ct);
    }
}

// Dispatcher
public class DomainEventDispatcher
{
    private readonly IServiceProvider _serviceProvider;

    public async Task DispatchAsync<TEvent>(TEvent domainEvent, CancellationToken ct)
        where TEvent : IDomainEvent
    {
        var handlers = _serviceProvider.GetServices<IDomainEventHandler<TEvent>>();

        foreach (var handler in handlers)
        {
            await handler.HandleAsync(domainEvent, ct);
        }
    }
}

Anti-Patterns to Avoid

God Class

A class that knows too much or does too much.

// BAD - God class
public class ApplicationManager
{
    public void CreateUser() { }
    public void DeleteUser() { }
    public void ProcessOrder() { }
    public void GenerateInvoice() { }
    public void SendEmail() { }
    public void BackupDatabase() { }
    public void GenerateReport() { }
    public void AuthenticateUser() { }
    public void ValidatePayment() { }
    // ... 50 more methods
}

// GOOD - Separate into focused classes
public class UserService { }
public class OrderService { }
public class InvoiceService { }
public class EmailService { }
public class BackupService { }
public class ReportService { }
public class AuthenticationService { }
public class PaymentService { }

Anemic Domain Model

Domain objects with only getters/setters and no behavior.

// BAD - Anemic model
public class Order
{
    public Guid Id { get; set; }
    public List<OrderItem> Items { get; set; }
    public OrderStatus Status { get; set; }
    public decimal Total { get; set; }
}

public class OrderService
{
    public void AddItem(Order order, Product product, int quantity)
    {
        order.Items.Add(new OrderItem { Product = product, Quantity = quantity });
        order.Total = order.Items.Sum(i => i.Product.Price * i.Quantity);
    }

    public void Submit(Order order)
    {
        if (order.Items.Count == 0)
            throw new InvalidOperationException("Cannot submit empty order");
        order.Status = OrderStatus.Submitted;
    }
}

// GOOD - Rich domain model
public class Order
{
    private readonly List<OrderItem> _items = new();

    public Guid Id { get; private set; }
    public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    public OrderStatus Status { get; private set; }
    public decimal Total => _items.Sum(i => i.Subtotal);

    public void AddItem(Product product, int quantity)
    {
        var existingItem = _items.FirstOrDefault(i => i.ProductId == product.Id);

        if (existingItem != null)
            existingItem.IncreaseQuantity(quantity);
        else
            _items.Add(new OrderItem(product, quantity));
    }

    public void Submit()
    {
        if (!_items.Any())
            throw new DomainException("Cannot submit empty order");

        Status = OrderStatus.Submitted;
        AddDomainEvent(new OrderSubmittedEvent(this));
    }
}

Service Locator

Using a global container to resolve dependencies.

// BAD - Service locator anti-pattern
public class OrderProcessor
{
    public void Process(Order order)
    {
        var repository = ServiceLocator.Get<IOrderRepository>();
        var emailService = ServiceLocator.Get<IEmailService>();
        var logger = ServiceLocator.Get<ILogger>();

        repository.Save(order);
        emailService.SendConfirmation(order);
        logger.Log("Order processed");
    }
}

// GOOD - Constructor injection
public class OrderProcessor
{
    private readonly IOrderRepository _repository;
    private readonly IEmailService _emailService;
    private readonly ILogger<OrderProcessor> _logger;

    public OrderProcessor(
        IOrderRepository repository,
        IEmailService emailService,
        ILogger<OrderProcessor> logger)
    {
        _repository = repository;
        _emailService = emailService;
        _logger = logger;
    }

    public async Task ProcessAsync(Order order)
    {
        await _repository.SaveAsync(order);
        await _emailService.SendConfirmationAsync(order);
        _logger.LogInformation("Order {OrderId} processed", order.Id);
    }
}

Magic Strings/Numbers

Using literals without explanation.

// BAD - Magic values
public class DiscountService
{
    public decimal Calculate(Order order)
    {
        if (order.Total > 100)
            return order.Total * 0.1m;
        if (order.CustomerType == "gold")
            return order.Total * 0.15m;
        return 0;
    }
}

// GOOD - Named constants
public class DiscountService
{
    private const decimal MinimumOrderForDiscount = 100m;
    private const decimal StandardDiscountRate = 0.10m;
    private const decimal GoldMemberDiscountRate = 0.15m;

    public decimal Calculate(Order order)
    {
        if (order.Customer.MembershipTier == MembershipTier.Gold)
            return order.Total * GoldMemberDiscountRate;

        if (order.Total >= MinimumOrderForDiscount)
            return order.Total * StandardDiscountRate;

        return 0;
    }
}

Primitive Obsession

Overusing primitives instead of small objects.

// BAD - Primitive obsession
public class Customer
{
    public string Email { get; set; }        // No validation
    public string PhoneNumber { get; set; }  // No format
    public decimal Balance { get; set; }     // No currency
}

public void SendEmail(string email) { }  // Any string accepted

// GOOD - Value objects
public record Email
{
    public string Value { get; }

    private Email(string value) => Value = value;

    public static Email Create(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new DomainException("Email is required");
        if (!value.Contains('@'))
            throw new DomainException("Invalid email format");
        return new Email(value.ToLowerInvariant());
    }

    public static implicit operator string(Email email) => email.Value;
}

public record Money
{
    public decimal Amount { get; }
    public Currency Currency { get; }

    private Money(decimal amount, Currency currency)
    {
        if (amount < 0)
            throw new DomainException("Amount cannot be negative");
        Amount = amount;
        Currency = currency;
    }

    public static Money Create(decimal amount, Currency currency) =>
        new(amount, currency);

    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new DomainException("Cannot add different currencies");
        return new Money(Amount + other.Amount, Currency);
    }
}

public class Customer
{
    public Email Email { get; private set; }
    public PhoneNumber Phone { get; private set; }
    public Money Balance { get; private set; }
}

Boolean Parameters

Using boolean flags that obscure meaning.

// BAD - What does true mean?
SendEmail(user, "Welcome!", true, false, true);

// GOOD - Use enums or separate methods
public enum EmailPriority { Low, Normal, High }

public record EmailOptions
{
    public bool IncludeUnsubscribeLink { get; init; } = true;
    public bool TrackOpens { get; init; } = true;
    public EmailPriority Priority { get; init; } = EmailPriority.Normal;
}

SendEmail(user, "Welcome!", new EmailOptions
{
    Priority = EmailPriority.High,
    TrackOpens = true
});

// Or use separate methods
SendWelcomeEmail(user);
SendPasswordResetEmail(user);

Industry Best Practices

Async/Await

// BAD
public Task<User> GetUserAsync(Guid id)
{
    return Task.Run(() => _repository.GetById(id)); // Don't wrap sync in Task.Run
}

public User GetUser(Guid id)
{
    return GetUserAsync(id).Result; // Deadlock risk
}

// GOOD
public async Task<User?> GetUserAsync(Guid id, CancellationToken ct = default)
{
    return await _repository.GetByIdAsync(id, ct);
}

// Async all the way
public async Task<IActionResult> GetUser(Guid id, CancellationToken ct)
{
    var user = await _userService.GetUserAsync(id, ct);
    return user is null ? NotFound() : Ok(user);
}

Null Handling

// BAD
public string GetDisplayName(User user)
{
    if (user != null && user.Profile != null && user.Profile.DisplayName != null)
        return user.Profile.DisplayName;
    return "Unknown";
}

// GOOD - Null-conditional and coalescing operators
public string GetDisplayName(User? user) =>
    user?.Profile?.DisplayName ?? "Unknown";

// GOOD - Nullable reference types
public class User
{
    public required string Email { get; init; }
    public string? DisplayName { get; set; }
    public Profile? Profile { get; set; }
}

// GOOD - Option/Maybe pattern for explicit optionality
public async Task<Option<User>> FindUserAsync(Guid id)
{
    var user = await _repository.GetByIdAsync(id);
    return user is null ? Option<User>.None : Option<User>.Some(user);
}

Exception Handling

// BAD - Catching and swallowing
try
{
    ProcessOrder(order);
}
catch (Exception ex)
{
    _logger.LogError(ex, "Error");
    // Silently continues...
}

// BAD - Catching too broadly
try
{
    ProcessOrder(order);
}
catch (Exception ex)
{
    return BadRequest(ex.Message); // Exposes internal errors
}

// GOOD - Catch specific, handle appropriately
try
{
    await ProcessOrderAsync(order, ct);
}
catch (ValidationException ex)
{
    _logger.LogWarning(ex, "Validation failed for order {OrderId}", order.Id);
    return BadRequest(new ProblemDetails
    {
        Title = "Validation Error",
        Detail = ex.Message,
        Status = 400
    });
}
catch (PaymentException ex)
{
    _logger.LogError(ex, "Payment failed for order {OrderId}", order.Id);
    return StatusCode(502, new ProblemDetails
    {
        Title = "Payment Processing Error",
        Detail = "Unable to process payment. Please try again.",
        Status = 502
    });
}
// Let unexpected exceptions propagate to global handler

Logging

// BAD - String interpolation (loses structured data)
_logger.LogInformation($"User {user.Id} logged in from {ipAddress}");

// BAD - Logging sensitive data
_logger.LogInformation("User login: {User}", JsonSerializer.Serialize(user));

// GOOD - Structured logging
_logger.LogInformation(
    "User {UserId} logged in from {IpAddress}",
    user.Id,
    ipAddress);

// GOOD - Log levels
_logger.LogDebug("Checking user permissions for {Resource}", resourceId);
_logger.LogInformation("User {UserId} created order {OrderId}", userId, orderId);
_logger.LogWarning("Rate limit approaching for {UserId}: {CurrentRate}/{MaxRate}", userId, current, max);
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
_logger.LogCritical(ex, "Database connection lost");

Configuration

// BAD - Hardcoded values
public class EmailService
{
    private readonly string _smtpServer = "smtp.example.com";
    private readonly int _port = 587;
}

// GOOD - Options pattern
public class EmailOptions
{
    public const string SectionName = "Email";

    public required string SmtpServer { get; init; }
    public int Port { get; init; } = 587;
    public required string FromAddress { get; init; }
    public bool UseSsl { get; init; } = true;
}

// appsettings.json
{
  "Email": {
    "SmtpServer": "smtp.example.com",
    "Port": 587,
    "FromAddress": "noreply@example.com"
  }
}

// Registration
services.Configure<EmailOptions>(config.GetSection(EmailOptions.SectionName));

// Usage
public class EmailService
{
    private readonly EmailOptions _options;

    public EmailService(IOptions<EmailOptions> options)
    {
        _options = options.Value;
    }
}

Disposable Resources

// BAD - Not disposing
public byte[] ReadFile(string path)
{
    var stream = new FileStream(path, FileMode.Open);
    var reader = new BinaryReader(stream);
    return reader.ReadBytes((int)stream.Length);
    // Stream never closed!
}

// GOOD - Using statement
public async Task<byte[]> ReadFileAsync(string path)
{
    await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true);
    var buffer = new byte[stream.Length];
    await stream.ReadAsync(buffer);
    return buffer;
}

// GOOD - Implement IDisposable properly
public class ResourceManager : IDisposable
{
    private readonly HttpClient _client = new();
    private bool _disposed;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            _client.Dispose();
        }

        _disposed = true;
    }
}

Quick Reference

Principle Remember
SRP One reason to change
OCP Extend, don't modify
LSP Subtypes are substitutable
ISP Small, focused interfaces
DIP Depend on abstractions
DRY Single source of truth
KISS Simple beats clever
YAGNI Build what you need now
Pattern Use When
Strategy Multiple algorithms, swap at runtime
Factory Complex object creation
Repository Abstract data access
Builder Complex object construction
Decorator Add behavior dynamically
Observer Event notifications
Anti-Pattern Solution
God Class Split into focused classes
Anemic Model Add behavior to entities
Service Locator Constructor injection
Magic Values Named constants
Primitive Obsession Value objects
Boolean Parameters Enums or options objects