Skip to content

Latest commit

 

History

History
541 lines (482 loc) · 15 KB

coding-style.md

File metadata and controls

541 lines (482 loc) · 15 KB
Error in user YAML: (<unknown>): did not find expected alphabetic or numeric character while scanning an alias at line 2 column 8
---
description: This file provides guidelines for writing clean, maintainable, and idiomatic C# code with a focus on functional patterns and proper abstraction.
globs: *.cs
---

Type Definitions:

  • Prefer records for data types:
    // Good: Immutable data type with value semantics
    public sealed record CustomerDto(string Name, Email Email);
    
    // Avoid: Class with mutable properties
    public class Customer
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }
  • Make classes sealed by default:
    // Good: Make classes sealed by default
    public sealed class OrderProcessor
    {
        // Implementation
    }
    
    // Only unsealed when inheritance is specifically designed for
    public abstract class Repository<T>
    {
        // Base implementation
    }

Variable Declarations:

  • Use var where possible:
    // Good: Using var for type inference
    var fruit = "Apple";
    var number = 42;
    var order = new Order(fruit, number);

Control Flow:

  • Prefer range indexers over LINQ:
    // Good: Using range indexers with clear comments
    var lastItem = items[^1];  // ^1 means "1 from the end"
    var firstThree = items[..3];  // ..3 means "take first 3 items"
    var slice = items[2..5];  // take items from index 2 to 4 (5 exclusive)
    
    // Avoid: Using LINQ when range indexers are clearer
    var lastItem = items.LastOrDefault();
    var firstThree = items.Take(3).ToList();
    var slice = items.Skip(2).Take(3).ToList();
  • Prefer collection initializers:
    // Good: Using collection initializers
    string[] fruits = ["Apple", "Banana", "Cherry"];
    
    // Avoid: Using explicit initialization when type is clear
    var fruits = new List<int>() {
        "Apple",
        "Banana",
        "Cherry"
    };
  • Use pattern matching effectively:
    // Good: Clear pattern matching
    public decimal CalculateDiscount(Customer customer) =>
        customer switch
        {
            { Tier: CustomerTier.Premium } => 0.2m,
            { OrderCount: > 10 } => 0.1m,
            _ => 0m
        };
    
    // Avoid: Nested if statements
    public decimal CalculateDiscount(Customer customer)
    {
        if (customer.Tier == CustomerTier.Premium)
            return 0.2m;
        if (customer.OrderCount > 10)
            return 0.1m;
        return 0m;
    }

Nullability:

  • Mark nullable fields explicitly:
    // Good: Explicit nullability
    public class OrderProcessor
    {
        private readonly ILogger<OrderProcessor>? _logger;
        private string? _lastError;
        
        public OrderProcessor(ILogger<OrderProcessor>? logger = null)
        {
            _logger = logger;
        }
    }
    
    // Avoid: Implicit nullability
    public class OrderProcessor
    {
        private readonly ILogger<OrderProcessor> _logger; // Warning: Could be null
        private string _lastError; // Warning: Could be null
    }
  • Use null checks only when necessary for reference types and public methods:
    // Good: Proper null checking
    public void ProcessOrder(Order order)
    {
        ArgumentNullException.ThrowIfNull(order); // Appropriate for reference types
    
        _logger?.LogInformation("Processing order {Id}", order.Id);
    }
    
    // Good: Using pattern matching for null checks
    public decimal CalculateTotal(Order? order) =>
        order switch
        {
            null => throw new ArgumentNullException(nameof(order)),
            { Lines: null } => throw new ArgumentException("Order lines cannot be null", nameof(order)),
            _ => order.Lines.Sum(l => l.Total)
        };
    // BAD: Avoid null checks for value types
    public void ProcessOrder(int orderId)
    {
        ArgumentNullException.ThrowIfNull(order); // DON'T USE Null checks are unnecessary for value types
    }
    
    // Avoid: null checks for non-public methods
    private void ProcessOrder(Order order)
    {
        ArgumentNullException.ThrowIfNull(order); // DON'T USE, ProcessOrder is private
    }
  • Use null-forgiving operator when appropriate:
    public class OrderValidator
    {
        private readonly IValidator<Order> _validator;
        
        public OrderValidator(IValidator<Order> validator)
        {
            _validator = validator ?? throw new ArgumentNullException(nameof(validator));
        }
        
        public ValidationResult Validate(Order order)
        {
            // We know _validator can't be null due to constructor check
            return _validator!.Validate(order);
        }
    }
  • Use nullability attributes:
    public class StringUtilities
    {
        // Output is non-null if input is non-null
        [return: NotNullIfNotNull(nameof(input))]
        public static string? ToUpperCase(string? input) =>
            input?.ToUpperInvariant();
        
        // Method never returns null
        [return: NotNull]
        public static string EnsureNotNull(string? input) =>
            input ?? string.Empty;
        
        // Parameter must not be null when method returns true
        public static bool TryParse(string? input, [NotNullWhen(true)] out string? result)
        {
            result = null;
            if (string.IsNullOrEmpty(input))
                return false;
                
            result = input;
            return true;
        }
    }
  • Use init-only properties with non-null validation:
    // Good: Non-null validation in constructor
    public sealed record Order
    {
        public required OrderId Id { get; init; }
        public required ImmutableList<OrderLine> Lines { get; init; }
        
        public Order()
        {
            Id = null!; // Will be set by required property
            Lines = null!; // Will be set by required property
        }
        
        private Order(OrderId id, ImmutableList<OrderLine> lines)
        {
            Id = id;
            Lines = lines;
        }
        
        public static Order Create(OrderId id, IEnumerable<OrderLine> lines) =>
            new(id, lines.ToImmutableList());
    }
  • Document nullability in interfaces:
    public interface IOrderRepository
    {
        // Explicitly shows that null is a valid return value
        Task<Order?> FindByIdAsync(OrderId id, CancellationToken ct = default);
        
        // Method will never return null
        [return: NotNull]
        Task<IReadOnlyList<Order>> GetAllAsync(CancellationToken ct = default);
        
        // Parameter cannot be null
        Task SaveAsync([NotNull] Order order, CancellationToken ct = default);
    }

Safe Operations:

  • Use Try methods for safer operations:

    // Good: Using TryGetValue for dictionary access
    if (dictionary.TryGetValue(key, out var value))
    {
        // Use value safely here
    }
    else
    {
        // Handle missing key case
    }
    // Avoid: Direct indexing which can throw
    var value = dictionary[key];  // Throws if key doesn't exist
    
    // Good: Using Uri.TryCreate for URL parsing
    if (Uri.TryCreate(urlString, UriKind.Absolute, out var uri))
    {
        // Use uri safely here
    }
    else
    {
        // Handle invalid URL case
    }
    // Avoid: Direct Uri creation which can throw
    var uri = new Uri(urlString);  // Throws on invalid URL
    
    // Good: Using int.TryParse for number parsing
    if (int.TryParse(input, out var number))
    {
        // Use number safely here
    }
    else
    {
        // Handle invalid number case
    }
    // Good: Combining Try methods with null coalescing
    var value = dictionary.TryGetValue(key, out var result)
        ? result
        : defaultValue;
    
    // Good: Using Try methods in LINQ with pattern matching
    var validNumbers = strings
        .Select(s => (Success: int.TryParse(s, out var num), Value: num))
        .Where(x => x.Success)
        .Select(x => x.Value);
  • Prefer Try methods over exception handling:

    // Good: Using Try method
    if (decimal.TryParse(priceString, out var price))
    {
        // Process price
    }
    
    // Avoid: Exception handling for expected cases
    try
    {
        var price = decimal.Parse(priceString);
        // Process price
    }
    catch (FormatException)
    {
        // Handle invalid format
    }

Asynchronous Programming:

  • Use Task.FromResult for pre-computed values:
    // Good: Return pre-computed value
    public Task<int> GetDefaultQuantityAsync() =>
        Task.FromResult(1);
    
    // Better: Use ValueTask for zero allocations
    public ValueTask<int> GetDefaultQuantityAsync() =>
        new ValueTask<int>(1);
    
    // Avoid: Unnecessary thread pool usage
    public Task<int> GetDefaultQuantityAsync() =>
        Task.Run(() => 1);
  • Always flow CancellationToken:
    // Good: Propagate cancellation
    public async Task<Order> ProcessOrderAsync(
        OrderRequest request,
        CancellationToken cancellationToken)
    {
        var order = await _repository.GetAsync(
            request.OrderId, 
            cancellationToken);
            
        await _processor.ProcessAsync(
            order, 
            cancellationToken);
            
        return order;
    }
  • Prefer await:
    // Good: Using await
    public async Task<Order> ProcessOrderAsync(OrderId id)
    {
        var order = await _repository.GetAsync(id);
        await _validator.ValidateAsync(order);
        return order;
    }
  • Never use Task.Result or Task.Wait:
    // Good: Async all the way
    public async Task<Order> GetOrderAsync(OrderId id)
    {
        return await _repository.GetAsync(id);
    }
    
    // Avoid: Blocking on async code
    public Order GetOrder(OrderId id)
    {
        return _repository.GetAsync(id).Result; // Can deadlock
    }
  • Use TaskCompletionSource correctly:
    // Good: Using RunContinuationsAsynchronously
    private readonly TaskCompletionSource<Order> _tcs = 
        new(TaskCreationOptions.RunContinuationsAsynchronously);
    
    // Avoid: Default TaskCompletionSource can cause deadlocks
    private readonly TaskCompletionSource<Order> _tcs = new();
  • Always dispose CancellationTokenSources:
    // Good: Proper disposal of CancellationTokenSource
    public async Task<Order> GetOrderWithTimeout(OrderId id)
    {
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
        return await _repository.GetAsync(id, cts.Token);
    }
  • Prefer async/await over direct Task return:
    // Good: Using async/await
    public async Task<Order> ProcessOrderAsync(OrderRequest request)
    {
        await _validator.ValidateAsync(request);
        var order = await _factory.CreateAsync(request);
        return order;
    }
    
    // Avoid: Manual task composition
    public Task<Order> ProcessOrderAsync(OrderRequest request)
    {
        return _validator.ValidateAsync(request)
            .ContinueWith(t => _factory.CreateAsync(request))
            .Unwrap();
    }

Symbol References:

  • Always use nameof operator:
    // Good: Using nameof in attributes
    public class OrderProcessor
    {
        [Required(ErrorMessage = "The {0} field is required")]
        [Display(Name = nameof(OrderId))]
        public string OrderId { get; init; }
        
        [MemberNotNull(nameof(_repository))]
        private void InitializeRepository()
        {
            _repository = new OrderRepository();
        }
        
        [NotifyPropertyChangedFor(nameof(FullName))]
        public string FirstName
        {
            get => _firstName;
            set => SetProperty(ref _firstName, value);
        }
    }
  • Use nameof with exceptions:
    public class OrderService
    {
        public async Task<Order> GetOrderAsync(OrderId id, CancellationToken ct)
        {
            var order = await _repository.FindAsync(id, ct);
            
            if (order is null)
                throw new OrderNotFoundException(
                    $"Order with {nameof(id)} '{id}' not found");
                    
            if (!order.Lines.Any())
                throw new InvalidOperationException(
                    $"{nameof(order.Lines)} cannot be empty");
                    
            return order;
        }
        
        public void ValidateOrder(Order order)
        {
            if (order.Lines.Count == 0)
                throw new ArgumentException(
                    "Order must have at least one line",
                    nameof(order));
        }
    }
  • Use nameof in logging:
    public class OrderProcessor
    {
        private readonly ILogger<OrderProcessor> _logger;
        
        public async Task ProcessAsync(Order order)
        {
            _logger.LogInformation(
                "Starting {Method} for order {OrderId}",
                nameof(ProcessAsync),
                order.Id);
                
            try
            {
                await ProcessInternalAsync(order);
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "Error in {Method} for {Property} {Value}",
                    nameof(ProcessAsync),
                    nameof(order.Id),
                    order.Id);
                throw;
            }
        }
    }

Usings and Namespaces:

  • Use implicit usings:
    // Good: Implicit
    namespace MyNamespace
    {
        public class MyClass
        {
            // Implementation
        }
    }
    // Avoid:
    using System; // DON'T USE
    using System.Collections.Generic; // DON'T USE
    using System.IO; // DON'T USE
    using System.Linq; // DON'T USE
    using System.Net.Http; // DON'T USE
    using System.Threading; // DON'T USE
    using System.Threading.Tasks;// DON'T USE
    using System.Net.Http.Json; // DON'T USE
    using Microsoft.AspNetCore.Builder; // DON'T USE
    using Microsoft.AspNetCore.Hosting; // DON'T USE
    using Microsoft.AspNetCore.Http; // DON'T USE
    using Microsoft.AspNetCore.Routing; // DON'T USE
    using Microsoft.Extensions.Configuration; // DON'T USE
    using Microsoft.Extensions.DependencyInjection; // DON'T USE
    using Microsoft.Extensions.Hosting; // DON'T USE
    using Microsoft.Extensions.Logging; // DON'T USE
    using Good: Explicit usings; // DON'T USE
    
    namespace MyNamespace
    {
        public class MyClass
        {
            // Implementation
        }
    }
  • Use file-scoped namespaces:
    // Good: File-scoped namespace
    namespace MyNamespace;
    
    public class MyClass
    {
        // Implementation
    }
    
    // Avoid: Block-scoped namespace
    namespace MyNamespace
    {
        public class MyClass
        {
            // Implementation
        }
    }