-
Notifications
You must be signed in to change notification settings - Fork 16
Style Guide
As this project is still in its infancy, this style guide will be changing frequently. This will also mean that your PRs may have a lot of continuous suggestions, as we are still trying to figure out what we really want the codebase to look like. Thanks for your understanding :)
These should all be covered by our rules in .editorconfig
as well as CSharpier. To use them, run a build if you're on Visual Studio, or run these in a terminal, relative to solution root:
# Solution-wide
dotnet csharpier .
dotnet format style
dotnet format whitespace
# Project-specific
dotnet csharpier LeaderboardBackend
dotnet format LeaderboardBackend style
dotnet format LeaderboardBackend whitespace
- All fields and methods MUST be PascalCase (static and non-static, public and private)
- Namespaces MUST match its folder path, replacing slashes for periods
- All files MUST use top-level namespaces
- Example:
// LeaderboardBackend/Models/Entities/ApplicationContext.cs namespace LeaderboardBackend.Models.Entities; public class ApplicationContext : DbContext
- Where explicitly mentioned below, doc comments MUST be added
- Line lengths are NOT limited; just use your best judgement
- Property attributes MUST be on their own line
-
var
MUST NOT be used; usenew()
instead - Early returns SHOULD be preferred
- All other C# style conventions MUST be used (e.g. opening braces in a new line)
- Entities mentioned in comments MUST be in lowercase (e.g. "user ID", not "User ID")
- Class name MUST be
<EntityNameInPlural>Controller
- E.g.: JudgementsController
- These attributes MUST be added:
[Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
We want the route to every controller be predictable, and we want JSON routes by default.
- Actions SHOULD be named as so:
Prefix HTTP Method Example Get GET GetRun Create POST CreateRun Update PUT/PATCH UpdateRun Delete DELETE DeleteRun - Actions SHOULD return ViewModels instead of Entities
- Attributes MUST be added in this order, as needed:
- (Required)
[ApiConventionMethod]
with the appropriate method, OR each[ProducesResponseType]
If a route produces something not covered in its relevant
[ApiConventionMethod]
, individual[ProducesResponseType]
s must be listed. - Authorisation policy or
[AllowAnonymous]
- (Required) API route method (e.g.
[HttpGet]
)
- (Required)
- For actions that return a list, it SHOULD 404 if both the parent entity couldn't be found and if the list is empty
- A message MUST be provided with the 404 to provide clarity
- Doc comments SHOULD be provided for fields
-
<summary>
,<param>
(if any), and<response>
MUST be added if so -
<remarks>
MAY be added too
-
- Example with authorisation policy (to allow anonymous requests):
// LeaderboardBackend/Controllers/UsersController.cs /// <summary>Gets a user by ID.</summary> /// <param name="id">The user's ID. It must be a GUID.</param> /// <response code="200">The user with the provided ID.</response> /// <response code="404">If no user is found with the provided ID.</response> [ApiConventionMethod(typeof(Conventions), nameof(Conventions.Get))] [AllowAnonymous] [HttpGet("{id:guid}")] public async Task<ActionResult<User>> GetUserById(Guid id)
- Example with authorisation policy:
// LeaderboardBackend/Controllers/JudgementsController.cs /// <summary>Creates a judgement for a run.</summary> /// <response code="201">The created judgement.</response> /// <response code="400">The request body is malformed.</response> /// <response code="404">For an invalid judgement.</response> [ApiConventionMethod(typeof(Conventions), nameof(Conventions.Post))] [Authorize(Policy = UserTypes.Mod)] [HttpPost] public async Task<ActionResult<JudgementViewModel>> CreateJudgement([FromBody] CreateJudgementRequest body)
- Requests MUST be public records with public fields
- A separate constructor can be added if needed
JSON serialisation works only on public fields with public getters and setters.
- Doc comments MUST be provided
-
<summary>
and<example>
MUST be added -
<remarks>
MAY be added too
-
- Examples:
/// <summary>Request object sent when creating a Category.</summary> public record CreateCategoryRequest { /// <summary>Name for the new Category.</summary> /// <example>Mongolian Throat Singing%</example> [Required] public string Name { get; set; } = null!; /// <summary> /// The bit in the URL that uniquely identifies this Category. <br/> /// E.g.: https://leaderboards.gg/slug-for-board/slug-for-category <br/> /// Must be 2-25 characters inclusive and only consist of letters, numbers, and /// hyphens. /// </summary> /// <example>mongolian-throat-singing</example> [Required] public string Slug { get; set; } = null!; // ... }
- ViewModels are response objects to controller actions
- ViewModels MUST be public records with public, required properties
- ViewModels MUST have a public
MapFrom(TDomainEntity entity)
method - Example:
// LeaderboardBackend/ViewModels/LeaderboardViewModel.cs /// <summary> /// Represents a collection of `Category` entities. /// </summary> public record LeaderboardViewModel { /// <summary> /// The unique identifier of the `Leaderboard`.<br/> /// Generated on creation. /// </summary> public required long Id { get; set; } /// <summary> /// The display name of the `Leaderboard` to create. /// </summary> /// <example>Foo Bar</example> public required string Name { get; init; } // ... public static LeaderboardViewModel MapFrom(Leaderboard leaderboard) { IList<CategoryViewModel>? categories = leaderboard.Categories? .Select(CategoryViewModel.MapFrom) .ToList(); return new LeaderboardViewModel { Id = leaderboard.Id, Name = leaderboard.Name, Slug = leaderboard.Slug, Rules = leaderboard.Rules, Categories = categories ?? Array.Empty<CategoryViewModel>(), }; } }
- Entities are our representation of database tables and objects
- Class names MUST be singular
- All fields MUST be public
- IDs MUST be spelt
Id
, and NOTID
- Required fields MUST be annotated with
[Required]
- Doc comments SHOULD be provided for fields
-
<summary>
MUST at least be provided -
<example>
and<remarks>
MAY be added
-
- Example fields, from minimal to maximal:
// LeaderboardBackend/Models/Entities/Leaderboard.cs /// <summary>Generated on creation.</summary> public long Id { get; set; } /// <summary>The Leaderboard's aka game's name. Pretty straightforward.</summary> /// <example>Mario Goes to Jail II</example> [Required] public string Name { get; set; } = null!; /// <summary> /// The bit in the URL after the domain that can be used to identify a Leaderboard. /// Meant to be human-readable. It must be: /// <ul> /// <li>between 2-80 characters, inclusive</li> /// <li>a string of characters separated by hyphens, if desired</li> /// </ul> /// </summary> /// <example>mario-goes-to-jail-ii</example> [Required] public string Slug { get; set; } = null!;
Shared ViewModel and Entity fields can have the same documentation.
- If creating a new model, name the migration
Create<EntityNameInPlural>
- If updating a model, name the migration
<EntityNameInPlural>_<Summary>
- Examples:
# Creating dotnet ef migrations -p LeaderboardBackend add -c ApplicationContext CreateParticipations # Updating dotnet ef migrations -p LeaderboardBackend add -c ApplicationContext Users_CompleteFields
- A service is an injectable class that contains EF Core logic
- Services SHOULD be the only place that contain direct DB interactions
- A service MUST be created in two steps, relative to
LeaderboardBackend/Services
:- As an interface named
I<ModelInSingular>Service.cs
- As its implementation named
Impl/<ModelInSingular>Service.cs
- As an interface named
- Example:
IBanService.cs
and its implementationImpl/BanService.cs
- Doc comments are NOT needed
- We only do public contract testing on our API
- Every endpoint SHOULD have a test for each of its possible responses
- Test names MUST be prefixed with
<ControllerAction_ResponseType>
- Example:
GetCategory_Unauthorized
Thanks for considering contributing to LB.GG 😌