Coding Standards¶
This guide outlines the coding standards and conventions for the LBS Foundry project. Following these standards ensures consistency, maintainability, and quality across the codebase.
C# Coding Standards¶
Member Access¶
- Always use
this.prefix: All class member access must usethis.prefix for properties, methods, and fields// Good public void Execute(CreatePlayerCommand command) { this.ValidateCommand(command); this.RaiseEvent(new PlayerCreatedEvent { PlayerId = this.playerId, PlayerName = command.PlayerName }); } // Bad public void Execute(CreatePlayerCommand command) { ValidateCommand(command); // Missing this. RaiseEvent(new PlayerCreatedEvent { PlayerId = playerId, // Missing this. PlayerName = command.PlayerName }); }
Field Naming¶
- No underscore naming: Private fields use camelCase without underscore prefix
- Exception for static fields: Static private fields may use underscore prefix when appropriate
Enum Usage¶
- No C# enums: Do not use C# enum types to avoid serialization issues and maintain API flexibility
- Use string constants: Replace enums with static classes containing string constants
// Good - String constants public static class UserStatus { public const string Active = "Active"; public const string Inactive = "Inactive"; public const string Suspended = "Suspended"; public const string Deleted = "Deleted"; public static readonly string[] All = { Active, Inactive, Suspended, Deleted }; public static bool IsValid(string status) => All.Contains(status); } // Usage public string Status { get; init; } = UserStatus.Active; // Bad - C# enum public enum UserStatus { Active, Inactive, Suspended, Deleted }
General Guidelines¶
- No emojis: Do not use emojis in code, comments, documentation, or any files unless explicitly requested by the user
- Nullable reference types: Always enabled (
<Nullable>enable</Nullable>) - Implicit usings: Always enabled (
<ImplicitUsings>enable</ImplicitUsings>)
Event Sourcing Patterns¶
Commands¶
[DataContract(Name = "CreateCompetition")]
[RequiresRoles(RoleDefinition.Admin)]
public sealed record CreateCompetitionCommand : DomainCommand<CompetitionId>
{
[DataMember] public string Sport { get; init; } = string.Empty;
[DataMember] public string Name { get; init; } = string.Empty;
[DataMember] public string Slug { get; init; } = string.Empty;
}
Events¶
[DataContract(Name = "CompetitionCreated", Namespace = LBSNamespace.Default)]
public sealed record CompetitionCreatedEvent : DomainEvent<CompetitionId>
{
[DataMember] public string Sport { get; init; } = string.Empty;
[DataMember] public string Name { get; init; } = string.Empty;
[DataMember] public string Slug { get; init; } = string.Empty;
[DataMember] public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
}
Aggregates¶
public sealed class CompetitionAggregate : AggregateRoot<CompetitionId>
{
private string name = string.Empty;
private string sport = string.Empty;
public void Execute(CreateCompetitionCommand command)
{
// Validation
if (string.IsNullOrEmpty(command.Name))
throw new ArgumentException("Competition name is required");
// Raise event
this.RaiseEvent(new CompetitionCreatedEvent
{
AggregateRootId = command.AggregateRootId,
Sport = command.Sport,
Name = command.Name,
Slug = command.Slug,
CreatedAt = DateTimeOffset.UtcNow
});
}
public void Apply(CompetitionCreatedEvent @event)
{
this.Id = @event.AggregateRootId;
this.name = @event.Name;
this.sport = @event.Sport;
}
}
Query Patterns¶
Query Definition¶
using System.Runtime.Serialization;
using System.Security.Claims;
using LBS.Anchor;
using LBS.Augment.Authentication;
[DataContract(Name = "PlayerSearch")]
public sealed class PlayerSearchQuery : ISearchQuery, IRequiresUserContext
{
public string QueryType => this.GetType().GetQueryTypeName();
public ClaimsPrincipal? User => UserContext.Current;
public string? SearchTerm { get; set; }
public List<string>? Teams { get; set; }
public decimal? MinSalary { get; set; }
// Pagination and ordering are carried on the query object
public string? OrderBy { get; set; }
public int Skip { get; set; }
public int? Take { get; set; }
}
Query Handler¶
The interface is IQueryHandler<in TQuery, TResult> with a single-argument HandleAsync(TQuery query) that returns a (Results, TotalCount) tuple. Pagination and ordering are properties of the query object, not separate parameters.
public sealed class PlayerSearchQueryHandler : IQueryHandler<PlayerSearchQuery, PlayerContract>
{
private readonly IDocumentSession session;
public PlayerSearchQueryHandler(IDocumentSession session)
{
this.session = session;
}
public async Task<(IEnumerable<PlayerContract> Results, int TotalCount)> HandleAsync(PlayerSearchQuery query)
{
var queryable = this.session.Query<PlayerContract>();
if (!string.IsNullOrEmpty(query.SearchTerm))
{
queryable = queryable.Where(p => p.PlayerName.Contains(query.SearchTerm));
}
if (query.Teams?.Any() == true)
{
queryable = queryable.Where(p => query.Teams.Contains(p.TeamName));
}
var totalCount = await queryable.CountAsync();
var results = await queryable.Skip(query.Skip).Take(query.Take ?? 100).ToListAsync();
return (results, totalCount);
}
}
TypeScript/Svelte Standards¶
Component Structure¶
<!-- PlayerCard.svelte -->
<script lang="ts">
import type { Player } from '$lib/types';
interface Props {
player: Player;
onSelect?: (playerId: string) => void;
}
let { player, onSelect }: Props = $props();
function handleClick() {
onSelect?.(player.id);
}
</script>
<button onclick={handleClick} class="player-card">
<h3>{player.name}</h3>
<p>{player.position}</p>
</button>
Naming Conventions¶
C# Naming¶
- Classes: PascalCase (e.g.,
PlayerAggregate,CompetitionService) - Interfaces: IPascalCase (e.g.,
ICommandExecutor,IQueryHandler) - Methods: PascalCase (e.g.,
Execute,HandleAsync) - Properties: PascalCase (e.g.,
PlayerName,CreatedAt) - Private fields: camelCase (e.g.,
logger,documentStore) - Constants: PascalCase or UPPER_CASE (e.g.,
DefaultTimeout,MAX_RETRIES) - Events: Past tense with "Event" suffix (e.g.,
PlayerCreatedEvent,TeamUpdatedEvent) - Commands: Present tense with "Command" suffix (e.g.,
CreatePlayerCommand,UpdateTeamCommand)
TypeScript Naming¶
- Components: PascalCase (e.g.,
PlayerCard,TeamList) - Functions: camelCase (e.g.,
handleClick,fetchPlayers) - Variables: camelCase (e.g.,
playerData,isLoading) - Types/Interfaces: PascalCase (e.g.,
Player,TeamStats) - Enums: Use string constants instead of TypeScript enums
Comments and Documentation¶
XML Documentation (C#)¶
/// <summary>
/// Creates a new player with the specified details.
/// </summary>
/// <param name="command">The player creation command containing player details.</param>
/// <returns>A list of domain events generated by the creation.</returns>
/// <exception cref="ArgumentException">Thrown when player name is empty.</exception>
public IReadOnlyList<IDomainEvent> Execute(CreatePlayerCommand command)
{
// Implementation
}
JSDoc (TypeScript)¶
/**
* Fetches player statistics for the specified team.
* @param teamId - The unique identifier of the team
* @param season - Optional season filter (defaults to current season)
* @returns Promise resolving to array of player statistics
*/
export async function fetchPlayerStats(
teamId: string,
season?: string
): Promise<PlayerStats[]> {
// Implementation
}
Testing Standards¶
Test Naming¶
[Test]
public async Task CreatePlayer_WithValidData_ShouldRaisePlayerCreatedEvent()
{
// Arrange
// Act
// Assert
}
[Test]
public async Task CreatePlayer_WithEmptyName_ShouldThrowArgumentException()
{
// Arrange
// Act & Assert
}
Test Organization¶
- Use AAA pattern (Arrange, Act, Assert)
- One assertion concept per test
- Use descriptive test names following the pattern:
MethodName_StateUnderTest_ExpectedBehavior - Group related tests in test fixtures
Code Organization¶
File Structure¶
src/
├── Domain/
│ └── LBS.Domain.Core/
│ └── Player/
│ ├── Commands/
│ │ └── CreatePlayerCommand.cs
│ ├── Events/
│ │ └── PlayerCreatedEvent.cs
│ ├── Aggregates/
│ │ └── PlayerAggregate.cs
│ ├── Contracts/
│ │ └── PlayerContract.cs
│ └── Configuration/
│ ├── PlayerMartenModule.cs
│ └── PlayerServiceExtensions.cs
Feature Module Pattern¶
Each feature should include:
1. [Feature]MartenModule.cs - Marten configuration
2. [Feature]ServiceExtensions.cs - DI registration
3. Commands, Events, and Aggregates in appropriate folders
4. Query handlers and contracts
Security Standards¶
Authorization¶
// Always specify required roles on commands
[RequiresRoles(RoleDefinition.Admin, RoleDefinition.UserManager)]
public sealed record CreateUserCommand : DomainCommand<UserId> { }
// Use ISecureQuery for authenticated queries
[RequiresRoles(RoleDefinition.Member)]
[DataContract(Name = "SecurePlayer")]
public sealed class SecurePlayerQuery : ISecureQuery
{
public string QueryType => this.GetType().GetQueryTypeName();
}
// Use IPublicQuery for public access
[DataContract(Name = "PublicTeam")]
public sealed class PublicTeamQuery : IPublicQuery
{
public string QueryType => this.GetType().GetQueryTypeName();
}
Data Protection¶
- Never log sensitive information (passwords, tokens, PII)
- Always validate input at command/query boundaries
- Use parameterized queries
- Follow OWASP guidelines
Performance Guidelines¶
Database Access¶
// Good - Efficient query with projection
var players = await this.session
.Query<PlayerContract>()
.Where(p => p.TeamId == teamId)
.Select(p => new { p.Id, p.Name, p.Position })
.ToListAsync();
// Bad - Loading unnecessary data
var players = await this.session
.Query<PlayerContract>()
.ToListAsync();
var filtered = players.Where(p => p.TeamId == teamId);
Async/Await¶
// Good - Proper async all the way
public async Task<PlayerContract?> GetPlayerAsync(PlayerId id)
{
return await this.session.LoadAsync<PlayerContract>(id.Value);
}
// Bad - Blocking async code
public PlayerContract? GetPlayer(PlayerId id)
{
return this.session.LoadAsync<PlayerContract>(id.Value).Result;
}
Version Control¶
Commit Messages¶
- Lead the subject with the Linear card id (e.g.
LBS-1839 - <summary>) - Do not use conventional-commit prefixes (
feat:,fix:,docs:,refactor:,test:) - Write the summary in present tense, describing what the change does
- List multiple cards comma-separated when a change spans several (e.g.
LBS-1770, LBS-1772 - ...)
Examples:
LBS-1839 - Add player statistics endpoint
LBS-1817 - Change clickhouse storage to use int[] instead of double[]
LBS-1770, LBS-1772 - Season hierarchical structure (Pools)
Pull Request Guidelines¶
- Include Linear card numbers in PR title
- Follow PR template
- Ensure all tests pass
- Request review from appropriate team members
Summary¶
These coding standards ensure: - Consistency: Uniform code style across the project - Maintainability: Easy to understand and modify - Quality: Reduced bugs and better performance - Security: Proper authorization and data protection - Team Efficiency: Clear expectations and patterns
Always prioritize readability and maintainability over clever solutions. When in doubt, follow the existing patterns in the codebase and consult with the team.