Skip to content

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 or ReSharper 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; }
      
  • 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);
          }
      }
      

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); 
      
  • 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() { ... }
      

Code Quality Tools

  • Static Analysis: SonarQube, Roslyn Analyzers.
  • CI/CD: Enforce standards via GitHub Actions/Azure Pipelines.