C# Coding Standards and Best Practices¶
This document outlines standardized C# coding conventions and best practices for PSS®X projects. It serves as a guideline for developers to write clean
, maintainable
, scalable
, and secure
code.
Project Structure (Modular, Layered Architecture)¶
The following diagram shows the packages of a well-layered module and dependencies of those packages between them:
- Do create a separated Visual Studio solution for every module.
- Do name the solution as GridLab.PSSX.ModuleName (for core ABP modules, it's Volo.Abp.ModuleName).
- Do develop the module as layered, so it has several packages (projects) those are related to each other.
- Every package has its own module definition file and explicitly declares the dependencies for the depended packages/modules.
Naming Conventions¶
Packages/Namespaces¶
- Standard: PascalCase, hierarchical.
- Pattern: CompanyName.ProjectName.Module
Classes¶
- Standard: PascalCase.
- Suffixes:
- Entities: <ModuleName>; User, Role
- Repositories: <ModuleName><Entity>Repository; IdentityUserRepository
- Service: <ModuleName><Entity>AppService, <ModuleName><Entity>DomainManager; IdentityUserAppService, IdentityUserManager
- Controller: <ModuleName><Entity>Controller; IdentityUserController
- DTOs: ...Dto for return objects where information hiding is required, ...Input for inputs which requires validation.
Methods¶
- Standard: PascalCase(verbs first, descriptive).
public List<User> GetAllUsers() { ... } public User GetUserById(int id) { ... }
Variables¶
- Standard: camelCase.
string userName; List<Role> assignedRoles;
Constants¶
- Standard: PascalCase.
public const string RoleAdmin = "Admin";
Enums¶
- Standard: PascalCase for type, PascalCase for values.
public enum UserStatus { Active, Inactive, Blocked }
Code Readability & Formatting¶
- Indentation: 4 spaces.
- Indentation Style: Spaces.
- Final Newline: Yes.
- Braces:
- Always use {}, single-line blocks can be skipped.
- Imports:
- Avoid wildcard imports; using System.*
- Use
.editorconfig
orReSharper
for consistency.
DTOs vs Entities¶
- Never expose
Entities
directly to controllers. - Use DTOs to map request/response data.
- Use AutoMapper for clean mapping.
Immutability¶
Immutability is a design principle where objects cannot be modified after creation.
DTOs¶
- Do prefer
records
for DTOs. - Do make all properties init-only.
[Serializable] public record IdentityUsersDto { public string Name{ get; init; } }
Entity¶
- Do keep core entity properties immutable.
- Do provide explicit mutation methods.
public class Order { public Guid Id { get; private set; } public OrderStatus Status { get; private set; } public DateTime CreatedAt { get; } public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly(); private readonly List<OrderItem> _items = new(); public Order(Guid id) { Id = id; CreatedAt = DateTime.UtcNow; Status = OrderStatus.Draft; } public void AddItem(OrderItem item) { // Validation logic here _items.Add(item); } public void MarkAsCompleted() { if (Status != OrderStatus.Paid) throw new InvalidOperationException("Only paid orders can be completed"); Status = OrderStatus.Completed; } }
Methods¶
- Do prefer pure functions that return new instances.
// Instead of mutating existing object: user.UpdateName("NewName"); // Avoid // Prefer returning new instance: public User WithUpdatedName(string newName) => this with { Username = newName };
Validation¶
- Validate all input DTOs before processing.
- Data Annotation Attributes:
- The most common approach using built-in attributes.
[Required(ErrorMessage = "Username is required")] [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")] public string Username { get; set; }
- The most common approach using built-in attributes.
- Fluent Validation:
- For complex validation scenarios, use FluentValidation library:
public class CreateUserInputValidator : AbstractValidator<CreateUserInput> { public CreateUserInputValidator() { RuleFor(x => x.Username) .NotEmpty().WithMessage("Username is required") .Length(3, 50).WithMessage("Username must be between 3 and 50 characters") .Must(BeUniqueUsername).WithMessage("Username already exists"); RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is required") .EmailAddress().WithMessage("Invalid email format"); RuleFor(x => x.Password) .NotEmpty().WithMessage("Password is required") .MinimumLength(8).WithMessage("Password must be at least 8 characters") .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter") .Matches("[a-z]").WithMessage("Password must contain at least one lowercase letter") .Matches("[0-9]").WithMessage("Password must contain at least one number"); RuleFor(x => x.Age) .InclusiveBetween(18, 120).WithMessage("Age must be between 18 and 120"); } private bool BeUniqueUsername(string username) { // Check against database or service return !_userService.UsernameExists(username); } }
- For complex validation scenarios, use FluentValidation library:
Logging¶
- Use ILogger interface (not Console.WriteLine).
- Use Serilog for structured logging library for .NET
- Structured logging:
- Use placeholders instead of string concatenation
logger.LogInformation("User created: {UserId}, {UserName}", user.Id, user.Name);
- Use placeholders instead of string concatenation
- Avoid Logging Sensitive Data.
Security¶
- Never hardcode secrets (use
IConfiguration
or Vault services like Azure Key Vault). - Avoid exposing internal IDs (use GUIDs for public APIs).
API Documentation¶
- Use Swagger/OpenAPI:
- Swagger UI makes API testing easy and encourages good documentation.
[SwaggerOperation(Summary = "Get all users")] [SwaggerResponse(200, "List of users", typeof(List<UserResponseDto>))] [HttpGet] public IActionResult GetAllUsers() { ... }
- Swagger UI makes API testing easy and encourages good documentation.
Code Quality Tools¶
- Static Analysis: SonarQube, Roslyn Analyzers.
- CI/CD: Enforce standards via GitHub Actions/Azure Pipelines.