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
| Rule | Description |
|---|
| Single Responsibility | One controller per page or feature area |
| No Business Logic | All logic lives in services |
| Async Throughout | All controller methods must be async Task<IActionResult> |
| Session Handling | Session/User context belongs in the controller, not services |
| ResponseModel | All responses use ResponseModel<T> — never raw JSON serialization |
| No Private Methods | If you need a helper, it belongs in a service |
| XML Documentation | Required on all public methods |
| Full Curly Braces | No 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
| Rule | Description |
|---|
| Return Type | All methods return Task<ResponseModel<T>> |
| Try-Catch | All methods with business logic or DB calls must have try-catch |
| AmazonLogger | Log request data, responses, and errors to CloudWatch |
| Async Throughout | All methods must be async Task<...> |
| Reuse Existing Services | Check for existing services before creating new ones |
| Parameter Encapsulation | Methods with more than 3 parameters use a DTO record |
| XML Documentation | Required 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
| Rule | Description |
|---|
| AsNoTracking | Use on all read-only queries — faster and prevents accidental saves |
| AsSplitQuery | Use with .Include() to avoid cartesian product issues |
| Projections | Select only needed fields — don’t return full entities for reads |
| Public = Service-Facing | Only methods called by services should be public |
| Method Length | Under 100 lines — split with private helpers if needed |
| Async Throughout | All methods must be async Task<...> |
| Log Queries | Log 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
| Element | Convention | Example |
|---|
| Classes, Records, Interfaces | PascalCase | RiskRegisterService |
| Public methods | PascalCase | GetRiskItemsAsync |
| Private methods | camelCase | buildFilterQuery |
| Variables and parameters | camelCase | projectId, 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
Controllers
Services
Repositories
DTOs and Data Structures
Testing