C# – Code Layout & Formatting
Consistent formatting makes code easier to read, review, and maintain. At Cygnus Dynamics, formatting is enforced automatically via .editorconfig, Roslyn analysers, and IDE settings — it should never be a point of debate in code review.
Automate it
Configure Visual Studio or Rider to format on save using the project's .editorconfig. Run dotnet format in CI to ensure every merged commit meets the formatting standard.
Indentation
Use 4 spaces per indentation level. Never use tabs — they render differently across editors and tools.
// ✅ 4-space indentation
public class OrderService
{
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
if (request.Items.Count == 0)
{
throw new ArgumentException("Order must have at least one item.");
}
var order = BuildOrder(request);
return await _repository.SaveAsync(order);
}
}
Brace Style — Allman
C# uses Allman style: every opening brace { appears on its own new line. This is the opposite of K&R style used in Java and JavaScript.
// ✅ Allman style — opening brace on its own line
public class OrderService
{
public Order GetOrderById(int orderId)
{
if (orderId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(orderId));
}
return _repository.FindById(orderId)
?? throw new OrderNotFoundException(orderId);
}
}
// ❌ K&R style — used in Java/JS, not C#
public class OrderService {
public Order GetOrderById(int orderId) {
if (orderId <= 0) {
throw new ArgumentOutOfRangeException(nameof(orderId));
}
}
}
Exception — expression-bodied members: For trivially simple single-expression members, use the => syntax:
// ✅ Expression-bodied for simple members
public string FullName => $"{FirstName} {LastName}";
public int ItemCount => _items.Count;
public override string ToString() => $"Order {{ Id = {Id}, Status = {Status} }}";
public bool IsEmpty() => !_items.Any();
Class Member Ordering
Within a class, members must appear in this order. Consistent ordering makes code predictable — reviewers and maintainers know exactly where to find things:
1. Static fields and constants
2. Instance fields (private)
3. Constructors (public first, then protected, then private)
4. Finaliser (~ClassName)
5. Properties (public first, then protected, then private)
6. Indexers
7. Events
8. Methods (public first, then protected, then private)
9. Explicit interface implementations
10. Nested types
// ✅ Correct member ordering
public sealed class OrderService : IOrderService
{
// 1. Static/constants
private const int MaxItemsPerOrder = 100;
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
// 2. Instance fields
private readonly IOrderRepository _orderRepository;
private readonly IPaymentService _paymentService;
private readonly ILogger<OrderService> _logger;
// 3. Constructor
public OrderService(
IOrderRepository orderRepository,
IPaymentService paymentService,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_paymentService = paymentService;
_logger = logger;
}
// 5. Properties
public string ServiceName => "OrderService";
// 7. Events
public event EventHandler<OrderCreatedEventArgs>? OrderCreated;
// 8. Public methods
public async Task<Order> GetOrderAsync(int orderId, CancellationToken ct = default) { }
public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default) { }
// 8. Protected methods
protected virtual void OnOrderCreated(OrderCreatedEventArgs e)
=> OrderCreated?.Invoke(this, e);
// 8. Private methods
private Order BuildOrder(CreateOrderRequest request) { }
private void ValidateRequest(CreateOrderRequest request) { }
}
No #region Blocks
Do not use #region / #endregion. If your class needs regions to be navigable, it is too large and should be split into smaller classes.
// ❌ Never use regions
#region Private Methods
private void ValidateRequest(CreateOrderRequest request) { }
private Order BuildOrder(CreateOrderRequest request) { }
#endregion
// ✅ If the class needs regions, split it instead
// OrderService.cs — orchestration only
// OrderValidator.cs — validation logic
// OrderBuilder.cs — construction logic
One Statement Per Line
// ✅ One statement per line
var user = await _userRepository.GetByIdAsync(userId);
var orders = await _orderRepository.GetByUserAsync(userId);
var total = CalculateTotal(orders);
// ❌ Multiple statements on one line
var user = await _userRepository.GetByIdAsync(userId); var orders = await _orderRepository.GetByUserAsync(userId);
One Declaration Per Line
// ✅ One declaration per line
int orderId;
string customerEmail;
decimal totalAmount;
// ❌ Multiple declarations on one line
int orderId, customerId, itemCount;
Line Length
Aim for lines under 120 characters. When wrapping, break before binary operators and align continuation logically:
// ✅ Breaking long method chains
var activeOrders = await _context.Orders
.Where(o => o.Status == OrderStatus.Confirmed)
.Where(o => o.CreatedAt >= cutoffDate)
.OrderByDescending(o => o.CreatedAt)
.Take(pageSize)
.ToListAsync(cancellationToken);
// ✅ Breaking long conditions
if ((user.IsActive && user.HasMarketingConsent)
|| (user.IsAdmin && request.IsOverride))
{
ProcessRequest(user, request);
}
// ✅ Breaking long method signatures
public async Task<PagedResult<OrderDto>> GetPagedOrdersAsync(
int userId,
OrderStatus? statusFilter,
int pageNumber,
int pageSize,
CancellationToken cancellationToken = default)
{
...
}
Blank Lines
using CygnusDynamics.Common.Exceptions;
using CygnusDynamics.Orders.Domain;
// ← one blank line after usings
namespace CygnusDynamics.Orders.Service;
// ← one blank line before class
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<OrderService> _logger;
// ← one blank line between field block and constructor
public OrderService(IOrderRepository orderRepository, ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
// ← one blank line between methods
public async Task<Order> GetOrderAsync(int orderId, CancellationToken ct = default)
{
return await _orderRepository.FindByIdAsync(orderId, ct)
?? throw new OrderNotFoundException(orderId);
}
// ← one blank line between methods
public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default)
{
// 1. Validate
ValidateRequest(request);
// 2. Build
var order = BuildOrder(request);
// ← blank line between logical sections inside method
// 3. Persist
await _orderRepository.SaveAsync(order, ct);
return order;
}
}
using Directives
Place using Outside the Namespace
// ✅ using outside namespace — fully qualified, unambiguous
using Azure.Identity;
using Microsoft.Extensions.Logging;
using CygnusDynamics.Orders.Domain;
namespace CygnusDynamics.Orders.Service;
public class OrderService { }
// ❌ using inside namespace — context-sensitive, fragile
namespace CygnusDynamics.Orders.Service
{
using Azure.Identity; // can be shadowed by CygnusDynamics.Orders.Azure if it exists
public class OrderService { }
}
Import Grouping Order
// Group 1 — System namespaces first
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
// Group 2 — Microsoft / third-party
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
// Group 3 — Internal (CygnusDynamics)
using CygnusDynamics.Common.Exceptions;
using CygnusDynamics.Orders.Domain;
using CygnusDynamics.Payments.Gateway;
Remove unused using directives before raising a PR. Visual Studio and Rider flag them automatically.
var — Implicit Typing
Use var when the type is immediately obvious from the right-hand side:
// ✅ Type is obvious — use var
var order = new Order();
var orders = new List<Order>();
var message = "Order confirmed";
var client = new HttpClient();
// ✅ Type is NOT obvious — use explicit type
int iterations = Convert.ToInt32(Console.ReadLine());
IEnumerable<Order> activeOrders = GetActiveOrders(); // explicit interface type matters
// ❌ var where type is unclear
var result = ProcessOrder(order); // what does ProcessOrder return? Use explicit type
String Comparison
Always specify StringComparison explicitly when comparing strings — the default behaviour varies by culture and platform:
// ✅ Explicit comparison — intent is unambiguous
if (currency.Equals("USD", StringComparison.OrdinalIgnoreCase))
ApplyUsdRules();
if (string.Equals(user.Email, inputEmail, StringComparison.OrdinalIgnoreCase))
throw new DuplicateEmailException();
// ✅ For ordering and sorting
var sorted = orders.OrderBy(o => o.Reference, StringComparer.OrdinalIgnoreCase).ToList();
// ✅ For URL, file path, and ID comparisons — use Ordinal
if (path.StartsWith("/api/v1", StringComparison.Ordinal))
return true;
// ❌ Implicit comparison — culture-dependent, different behaviour on Linux vs Windows
if (currency == "usd") // case-sensitive, exact match only
ApplyUsdRules();
if (currency.ToLower() == "usd") // ToLower() is culture-dependent — use OrdinalIgnoreCase instead
ApplyUsdRules();
| Scenario | Use |
|---|---|
| User-visible text, culture-sensitive sorting | StringComparison.CurrentCultureIgnoreCase |
| Internal identifiers, codes, email, paths | StringComparison.OrdinalIgnoreCase |
| File paths, URLs, binary protocol values | StringComparison.Ordinal |
| Dictionary keys for codes/IDs | StringComparer.OrdinalIgnoreCase |
Object and Collection Initialisers
// ✅ Object initialiser
var order = new Order
{
UserId = currentUser.Id,
Currency = "USD",
Status = OrderStatus.Pending,
CreatedAt = DateTimeOffset.UtcNow,
};
// ✅ Collection expressions (C# 12)
string[] currencies = ["USD", "EUR", "GBP", "AED"];
List<int> itemIds = [1, 2, 3, 4, 5];
// ✅ Target-typed new (C# 9+) — type inferred from declaration
Order order = new();
List<OrderItem> items = new();
// ❌ Verbose manual assignments
var order = new Order();
order.UserId = currentUser.Id;
order.Currency = "USD";
using Statements for Disposable Resources
// ✅ Declaration-style using (C# 8+) — disposed at end of scope
using var connection = new SqlConnection(_connectionString);
using var command = connection.CreateCommand();
await connection.OpenAsync(cancellationToken);
// ✅ Traditional using — when you need explicit scope control
using (var stream = File.OpenRead(filePath))
{
return await ParseOrdersAsync(stream);
}
// stream disposed here — before method exit
// ❌ Manual dispose — resource may leak if exception is thrown
var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
// ... if this throws, connection never closes
connection.Dispose();
Parentheses for Clarity
// ✅ Explicit grouping — intent is unambiguous
if ((startX > endX) && (startY > endY))
ProcessRegion();
decimal discounted = basePrice * (1 - discountRate);
Modifier Order
Per Roslyn convention — when multiple modifiers are applied, use this order:
public / protected / internal / private
new
abstract / virtual / override / sealed
static
readonly / extern
unsafe / volatile
async
// ✅ Correct modifier order
public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
protected virtual async Task<Order> FetchOrderAsync(int id) { }
private static readonly ILogger s_logger = LoggerFactory.Create(...).CreateLogger("App");
// ❌ Wrong order
static public readonly int MaxRetry = 3;
async protected virtual Task<Order> FetchOrderAsync(int id) { }
Recommended .editorconfig
Every C# project must include an .editorconfig at the solution root:
[*.cs]
# Indentation
indent_style = space
indent_size = 4
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = true
# Allman braces
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
# using directives
csharp_using_directive_placement = outside_namespace:warning
# var preferences
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = false:suggestion
# Expression-bodied members
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion
# No regions
dotnet_diagnostic.IDE0065.severity = warning # using directive placement
# Configure IDE to flag regions via Roslyn:
csharp_style_prefer_range_operator = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
# String comparison
dotnet_diagnostic.CA1307.severity = warning # StringComparison missing (ordinal-less overload)
dotnet_diagnostic.CA1309.severity = warning # Use ordinal StringComparison
# Naming
dotnet_naming_rule.private_fields_should_be_camel_case.severity = warning
dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields
dotnet_naming_rule.private_fields_should_be_camel_case.style = underscore_camel_case
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.underscore_camel_case.capitalization = camel_case
dotnet_naming_style.underscore_camel_case.required_prefix = _
# Async suffix
dotnet_naming_rule.async_methods_must_have_async_suffix.severity = warning
dotnet_naming_rule.async_methods_must_have_async_suffix.symbols = async_methods
dotnet_naming_rule.async_methods_must_have_async_suffix.style = async_suffix_style
dotnet_naming_symbols.async_methods.applicable_kinds = method
dotnet_naming_symbols.async_methods.required_modifiers = async
dotnet_naming_style.async_suffix_style.required_suffix = Async