4R Digital Help

Coding Standards

Prefer asynchronous calls whenever possible

Asynchronous calls are assigned to separate threads and managed by TPL. This frees up the main thread and makes the software more responsive.

Not all cases need to be asynchronous, but in cases of intensive processing or multi-user access (e.g., APIs), it is recommended to use asynchronous calls whenever possible.

When using async, do it all the way

Building on the above item, once an asynchronous call is necessary, all subsequent calls that have this capability should be used asynchronously.

Interrupting this asynchronicity midway ends up making the entire flow synchronous, as the current thread needs to be locked while waiting for a result.

Prefer Ternaries and Null Coalesce

Both structures simplify and facilitate code readability.

Ternary:

public decimal CalculateCost(int hours, decimal value) => hours > 0 ? value * hours : 0;

Null Coalesce:

public decimal CalculateCost(uint? hours, decimal value) { var totalHours = hours ?? 0; return totalHours * value; }

Prefer string interpolation

String interpolation allows easy and practical formatting of strings.

Guards

A Guard is a validation call that tests data and breaks the flow when the condition is not met.
They are primarily used for input and output parameter validations to strengthen software consistency through Defensive Programming.

An excellent library for this is Ardalis.GuardClauses, although we have built a wrapper around to extend it and make it feature complete.

public Customer UpdateCustomerCode(string id) { Guard.Against.Null(id, "Parameter cannot have a null value"); Guard.Against.NullOrEmpty(id, "Parameter cannot have an empty value"); Guard.Against.NullOrWhiteSpace(id, "Parameter cannot have a whitespace value"); Guard.Against.NegativeOrZero(id, "Parameter must not be zero or negative"); var customer = this._customerRepository.Get(id); Guard.Ensure.NotNull(customer, "Could not find a customer by id."); return customer; }

Break chains of calls into separate lines

You typically won't use method chains, but well-known ones include LINQ and other Fluent APIs.
Break these calls into separate lines to facilitate readability, especially when including lambdas.

Hard to read:

var result = collection.Where(item => item.Total > 100).Select(item => item.Name);

Easy to read:

var result = collection .Where(item => item.Total > 100) .Select(item => item.Name);

Default language for code is English for new projects

Regardless of the language you speak, the default language for code should be English.
Exceptions are domain-specific names for adherence to ubiquitous language.

The reasons for this are as follows:

  1. Consistency: All programming languages on the market were written in English, so we should adhere as much as possible to maintain code consistency.

  2. Convenience: All market standards were also conceived in this language, making it much more complicated to translate or adapt terms.

Avoid the use of abbreviations

Do not abbreviate names or use acronyms that are not commonly known to all who may be involved with the code.
Also, avoid type or scope affixes, such as strText.

Avoid in-liners for readability

In-liners (operations that can be written in a single line) are good only when they are not complex.
Complex in-liners reduce readability, especially for new developers on the team.
As a general rule, avoid complex ones.

No matter if the code becomes more "extensive" as long as it is easy for others to read, understand, and maintain.

Bad:

public void Main() { var result = this.service.Process(new ProcessCalculation(entry => entry.Total ^ 100, new Counter(valueAlpha, 10), new RecursiveExample(new DataModel( value1, value2, value3 ), 12) )); }

Good:

public void Main() { const int counterDelta = 10; const int recursiveCount = 12; var processCounter = new Counter(valueAlpha, counterDelta); var model = new DataModel(value1, value2, value3); var recursiveExample = new RecursiveExample(model, recursiveCount); var calculatedProcess = new ProcessCalculation(this.CalculateTotal, processCounter, recursiveExample); this.service.Process(calculatedProcess); } private double CalculateTotal(double total) => total ^ 100;

Add comments to public scopes for documentation purposes

Add documentation comments (<summary>) to all public scopes (Classes, Methods, Properties, etc.).
Include related tags, such as params, returns, exceptions, etc.

It is always very important to mention in the exceptions tag all exceptions that can be thrown, directly or indirectly, by the scope.

/// <summary> /// Domain service class for customers. /// </summary> public sealed class CustomerService : ICustomerService { private readonly ICustomerRepository repository; /// <summary> /// Creates a new instance of <see cref="CustomerService"/>. /// </summary> /// <param name="repository">Injected instance of the customer repository.</param> /// <exception cref="ArgumentNullException">When <paramref name="repository"/> is null.</exception> public CustomerService(ICustomerRepository repository) { this.repository = Guard.Against.Null(repository); } /// <summary> /// Deletes an existing customer from the database. /// </summary> /// <param name="customer">Instance of the customer to be deleted.</param> /// <returns>Returns <c>true</c> if deleted successfully, otherwise <c>false</c>.</returns> /// <exception cref="ArgumentNullException">When <paramref name="repository"/> is null.</exception> public bool Delete(Customer customer) { Guard.Against.Null(customer); return this.repository.Delete(customer); } }

Low Coupling, High Cohesion

Prefer independent components (decoupled) with well-defined purposes.
Following the SOLID principles is already adhering well to this guideline.

A .cs file should generally contain only one class.
Exceptions:

  • Generic classes with the same name but different type parameters;

  • Sub-classes when necessary.

Avoid logic in property gets/sets

Properties should not contain logic inside, working similarly to variables. Code inside them is difficult to visualize and trace.
For cases where there is a need, such as common cases of notification with PropertyChangedNotify, use methods called by the property.

The FluentAssertions library makes writing test validations easier and more fluent.

Standard:

Assert.GreaterOrEqual(employee.Salary, 5000, "This is the lowest salary we pay.");

FluentAssertions:

employee.Salary.Should().BeEqualOrGreaterThan(5000, because: "This is the lowest salary we pay.");

Test pattern: Given/When/Then

This pattern makes it easy to test the right parts while providing useful information about the test case.

Example:

[Theory] [InlineData(default(string))] [InlineData(default(""))] [InlineData(default(" "))] public void GivenInvalidId_WhenQueryingCustomers_ThenExpectArgumentNullException(string? id) { // Arrange var sutService = new CustomerService(); // Act async Task SutCall() { await sutService.Get(id); } Func<Task> sutCall = SutCall; // Assert return sutCall.Should().ThrowExactlyAsync<ArgumentNullException>("Parameter is required to process the request."); }

Class and method versioning

In many cases, there is a need to maintain different versions of the same code for compatibility reasons.
The correct way is to version with namespaces, allowing the choice of version when in use.

Example:

namespace System.V1 { public class Calculator { // Code } }

Corporate Standards

Version routes

Routes should be versioned with "/vX/", where X is the version number. This will help introduce new versions and maintain compatibility.

Adhere to RESTful API standards

  1. Do not use verbs in routes, but prefer http verbs

Bad:

Good:

  1. Avoid separators ("-") in favor of nested routes

Bad:

Good:

  1. Use filters as query string parameters

  1. Use HTTP verbs to define the type of action

  • GET: Queries by id or listings

  • POST: Entity creation or complex executions

  • PUT: Entity modification

  • DELETE: Entity removal

  1. Use consistent error codes:

  • 20X: Success

  • 40X: Client did something wrong

  • 50X: The System behaved unexpectedly

Going deeper:

Examples:

Data Validation

We use FluentValidations in our APIs to validate inputs. It is a great library as it is very flexible and decouples the validation logic from the current code where data is used.

Last modified: 03 June 2024