Skip to main content

Why We Have Coding Standards

Coding standards at Dash360 exist to ensure all code is consistent, maintainable, and high quality. The key outcomes:
  • Consistency — Any developer can understand any part of the codebase quickly
  • Maintainability — Clean structure reduces bugs and makes refactoring safe
  • Better code reviews — Reviews focus on logic and architecture, not style
  • Testability — Single-responsibility and DI patterns make unit testing straightforward
  • Faster onboarding — New developers have a clear, documented path forward

Controller Standards

Core Principle: Controllers Are Traffic Cops

Controllers route HTTP requests to services. They contain no business logic.
A controller action should be a single service call.
/// <summary>
/// Gets the risk register view for the specified project.
/// </summary>
public async Task<IActionResult> Index(int projectId)
{
    return Ok(await _riskRegisterService.GetRiskRegisterAsync(projectId, User));
}

Rules

RuleDescription
Single ResponsibilityOne controller per page or feature area
No Business LogicAll logic lives in services
Async ThroughoutAll controller methods must be async Task<IActionResult>
Session HandlingSession/User context belongs in the controller, not services
ResponseModelAll responses use ResponseModel<T> — never raw JSON serialization
No Private MethodsIf you need a helper, it belongs in a service
XML DocumentationRequired on all public methods
Full Curly BracesNo arrow functions or expression-bodied method declarations

Naming

  • Public methods: PascalCase (GetRiskItems, SaveRiskItem)
  • Parameters and variables: camelCase (projectId, riskItem)
  • Branch names: DAS-[ticket-number]-description (uppercase DAS- prefix required)

Service Layer Standards

Core Principle: All Business Logic Lives Here

Services implement the work that controllers delegate to them.
/// <summary>
/// Retrieves all risk items for the specified project.
/// </summary>
/// <param name="projectId">The project to query.</param>
/// <param name="user">The current user for authorization context.</param>
/// <returns>A ResponseModel containing the list of risk items.</returns>
public async Task<ResponseModel<List<RiskItemDto>>> GetRiskItemsAsync(int projectId, ClaimsPrincipal user)
{
    try
    {
        _logger.LogInformation("GetRiskItems called for project {ProjectId}", projectId);
        var items = await _riskRepository.GetByProjectAsync(projectId);
        return ResponseModel<List<RiskItemDto>>.Success(items);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error retrieving risk items for project {ProjectId}", projectId);
        return ResponseModel<List<RiskItemDto>>.Error("Failed to retrieve risk items.");
    }
}

Rules

RuleDescription
Return TypeAll methods return Task<ResponseModel<T>>
Try-CatchAll methods with business logic or DB calls must have try-catch
AmazonLoggerLog request data, responses, and errors to CloudWatch
Async ThroughoutAll methods must be async Task<...>
Reuse Existing ServicesCheck for existing services before creating new ones
Parameter EncapsulationMethods with more than 3 parameters use a DTO record
XML DocumentationRequired on all public methods

Naming

  • Public methods: PascalCase (GetRiskItemsAsync)
  • Private methods: camelCase (buildRiskQuery)
  • Interfaces: Prefix with I (IRiskRegisterService)

Repository Layer Standards

Core Principle: Data Access Only

Repositories translate between EF Core entities and the application.
/// <summary>
/// Retrieves all risk items for a project (read-only).
/// </summary>
public async Task<List<RiskItemDto>> GetByProjectAsync(int projectId)
{
    return await _context.RiskItems
        .AsNoTracking()
        .AsSplitQuery()
        .Where(r => r.ProjectId == projectId)
        .Select(r => new RiskItemDto
        {
            Id = r.Id,
            Title = r.Title,
            Status = r.Status
        })
        .ToListAsync();
}

Rules

RuleDescription
AsNoTrackingUse on all read-only queries — faster and prevents accidental saves
AsSplitQueryUse with .Include() to avoid cartesian product issues
ProjectionsSelect only needed fields — don’t return full entities for reads
Public = Service-FacingOnly methods called by services should be public
Method LengthUnder 100 lines — split with private helpers if needed
Async ThroughoutAll methods must be async Task<...>
Log QueriesLog queries and results for transparency
AsNoTracking() returns read-only entities. Do not use them for update or delete operations — re-query without AsNoTracking when you need to modify data.

Code Structure Standards

DTOs Use Records

// ✅ Correct — use records for immutability and reduced boilerplate
public record RiskItemDto(int Id, string Title, string Status);

// ❌ Avoid — classes add unnecessary boilerplate for DTOs
public class RiskItemDto { public int Id { get; set; } ... }

Naming Conventions

ElementConventionExample
Classes, Records, InterfacesPascalCaseRiskRegisterService
Public methodsPascalCaseGetRiskItemsAsync
Private methodscamelCasebuildFilterQuery
Variables and parameterscamelCaseprojectId, riskItem

Method Length

All methods should be under 100 lines. Use private helper methods with descriptive names for complex operations.

Spacing

  • No extra blank line after opening brace {
  • No extra blank line before closing brace }
  • One blank line between methods
  • One blank line between logical blocks within a method

JSON / API Casing

ASP.NET Core serializes JSON responses in camelCase by default. Always use camelCase when reading API responses in JavaScript.
// ❌ WRONG — C# property names (PascalCase)
if (data.ReportConfig) { }
if (config.CalculatedFields) { }

// ✅ CORRECT — JSON camelCase
if (data.reportConfig) { }
if (config.calculatedFields) { }

PR Checklist

General Quality

  • Code is clean, concise, and follows DRY
  • All public methods have XML documentation
  • Comments exist only where logic isn’t self-evident
  • No method exceeds 100 lines

Controllers

  • Acts only as traffic cop — delegates all logic to services
  • Session handling (User, ProjectId) managed in controller
  • Controller methods reduced to single service call
  • All responses use ResponseModel<T>
  • All methods are async
  • Full curly braces — no arrow functions
  • No private methods

Services

  • All methods return Task<ResponseModel<T>>
  • All business logic in try-catch blocks
  • AmazonLogger used for request/response/error logging
  • No duplicated logic — existing services reused where possible
  • Methods with 3+ parameters use DTO record
  • Public methods PascalCase, private camelCase

Repositories

  • AsNoTracking() used on all read-only queries
  • AsSplitQuery() used with navigation properties
  • Only necessary fields projected — no full entities for reads
  • Only service-facing methods are public
  • Queries and results logged to CloudWatch
  • All methods async

DTOs and Data Structures

  • Records used for all DTOs
  • Classes/records PascalCase, variables camelCase
  • Complex parameter lists encapsulated in DTO records

Testing

  • Unit tests exist for new services and repositories
  • Dependencies are mockable (interface-based)
  • Error scenarios tested with appropriate ResponseModel.ErrorMessage assertions