Skip to content

Style Guide

zysim edited this page Apr 29, 2023 · 11 revisions

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

General

  • 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; use new() 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")

Controllers

  • 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.

Controller Actions

  • 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:
    1. (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.

    2. Authorisation policy or [AllowAnonymous]
    3. (Required) API route method (e.g. [HttpGet])
  • 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

  • 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

  • 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

  • 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 NOT ID
  • 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.

Migrations

  • 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

Services

  • 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:
    1. As an interface named I<ModelInSingular>Service.cs
    2. As its implementation named Impl/<ModelInSingular>Service.cs
  • Example: IBanService.cs and its implementation Impl/BanService.cs
  • Doc comments are NOT needed

Tests

  • 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
Clone this wiki locally