Rulegeneral

Backend Rule

Core rules for C# development and tooling

View Source

Backend

Guidelines for C# backend development, including code style, naming, exceptions, logging, and build/test/format workflow.

Code Style

  • Be consistent—if you do something a certain way, do all similar things the same way
  • Always use these C# features:
    • Top-level namespaces
    • Primary constructors
    • Array initializers
    • Pattern matching with is null and is not null instead of == null and != null
  • Records for immutable types
  • Mark all C# types as sealed
  • Use var when possible
  • Use simple collection types like UserId[] instead of List<UserId> whenever possible
  • JetBrains tooling formats code automatically, but line breaking is disabled for readability:
    • Wrap lines only when new language constructs start after 120 characters (content after 120 chars is acceptable if the construct starts before)
    • CancellationToken cancellationToken is not considered important and should never trigger a line break
    • Prefer long lines over splitting to maximize visible code—only wrap when truly necessary
    • Examples:
      // ✅ DO: Keep on one line even if longer than 120 chars (the 'c' in 'command' is before 120 chars)
      public async Task<Result<CompleteEmailConfirmationResponse>> Handle(CompleteEmailConfirmationCommand command, CancellationToken cancellationToken)
      
      // ✅ DO: Keep constructor parameters on one line when they fit (the 'e' in 'executionContext' is before 120 chars)
      public sealed class GetPaymentHistoryHandler(ISubscriptionRepository subscriptionRepository, IExecutionContext executionContext)
          : IRequestHandler<GetPaymentHistoryQuery, Result<PaymentHistoryResponse>>
      
      // ✅ DO: Wrap to 2 lines when needed, but never 3, 4, or 5 lines
      var updatedLocale = Connection.ExecuteScalar<string>(
          "SELECT Locale FROM Users WHERE Id = @id", new { id = DatabaseSeeder.Tenant1Owner.Id.ToString() }
      );
      
      // ❌ DON'T: Split method parameters across multiple lines when they fit before 120 chars
      public async Task<Result> Handle(
          UpgradeSubscriptionCommand command,
          CancellationToken cancellationToken
      )
      
      // ❌ DON'T: Split constructor parameters across multiple lines when they fit before 120 chars
      public sealed class GetPaymentHistoryHandler(
          ISubscriptionRepository subscriptionRepository,
          IExecutionContext executionContext
      ) : IRequestHandler<GetPaymentHistoryQuery, Result<PaymentHistoryResponse>>
      
  • Avoid using exceptions for control flow:
    • When throwing exceptions, use meaningful exceptions following .NET conventions
    • Use UnreachableException to signal unreachable code that cannot be reached by tests
    • Exception messages should include a period
  • Log only meaningful events at appropriate severity levels:
    • Logging messages should not include a period
    • Use structured logging
  • Never introduce new NuGet dependencies
  • Don't do defensive coding (e.g., don't add exception handling for situations we don't know will happen)
  • Use user?.IsActive == true over user != null && user.IsActive == true
  • Avoid try-catch unless we cannot fix the root cause—global exception handling covers unknown exceptions
  • Use SharedInfrastructureConfiguration.IsRunningInAzure to determine if running in Azure
  • Inject TimeProvider into services and handlers, use timeProvider.GetUtcNow() instead of DateTimeOffset.UtcNow
  • Pass DateTimeOffset values (not TimeProvider) to domain methods and aggregates to maintain clean boundaries (e.g., entity.HasExpired(timeProvider.GetUtcNow()))
  • Naming rules:
    • Never use acronyms or abbreviations (e.g., use SharedAccessSignature not Sas, Context not Ctx)
    • Prefer long variable names for readability (e.g., gravatarHttpClient not httpClient)
    • Choose descriptive and unambiguous names
    • Make meaningful distinctions
    • Use pronounceable names
    • Use searchable names
    • Replace magic numbers with named constants
    • Avoid encodings—don't append prefixes or type information
  • Comments rules:
    • Don't explain what you changed (that belongs in commit messages)—code should reflect the current state only
    • Always try to explain yourself in code
    • Don't be redundant
    • Don't add obvious noise
    • Don't use closing brace comments
    • Don't comment out code—just remove it
    • Use comments for explanation of intent, clarification, or warning of consequences
  • Source code structure:
    • Separate concepts vertically
    • Related code should appear vertically dense
    • Declare variables close to their usage
    • Dependent functions should be close
    • Similar functions should be close
    • Place functions in the downward direction
    • Order: public functions above internal, internal above private
    • Don't use horizontal alignment
    • Use white space to associate related things and disassociate weakly related
    • Avoid nesting—prefer early return or break/continue statements, keeping the happy path at the end
  • Functions rules:
    • Keep them small
    • Do one thing
    • Use descriptive names
    • Prefer fewer arguments
    • Have no side effects
    • Don't use flag arguments—split into independent methods instead
  • For enum comparisons:
    • When comparing enums to enums, use direct comparison: tenant.State == tenantState.Trial
    • When comparing string properties to enums (e.g., JWT claims), use nameof: executionContext.UserInfo.Role == nameof(UserRole.Owner)
    • Avoid unnecessary Enum.TryParse when the comparison context is clear

Implementation

Follow these steps when implementing changes:

  1. Always start new changes by writing new test cases (or change existing tests)—consult API Tests for details
  2. Build and test your changes:
    • Use the execute MCP tool with command: "build" for backend
    • Use the execute MCP tool with command: "test" to run all tests
    • If you change API contracts (endpoints, DTOs), also build frontend to ensure it still compiles
  3. Format and inspect your code in parallel:
    • When all tests pass and the feature is complete, call both MCP tools in a single message:
      • execute_command(command: "format")
      • execute_command(command: "inspect")
    • Format automatically fixes code style issues according to our conventions
    • ALL inspect findings are blocking - CI pipeline fails on any result marked "Issues found"
    • Severity level (note/warning/error) is irrelevant - fix all findings before proceeding

When you see paths like /[scs-name]/Core/Features/[Feature]/Domain in rules, replace [scs-name] with the specific self-contained system name (e.g., account-management, back-office) and [Feature] with the feature name (e.g., Users, Tenants). A feature is often 1:1 with a domain aggregate.