Version: 1.0.0
Last Updated: 2024-11-12
This document describes architectural discipline rules for C# codebases.
ADP analyzes C# source files (.cs) to ensure maintainable, well-structured code following Microsoft's design guidelines and industry best practices.
- Object-oriented design principles (SOLID)
- Interface-based programming
- Property and method conventions
- LINQ and functional patterns
- Async/await patterns
Thresholds for C#:
| File Type | Expected Range | Description |
|---|---|---|
| Class | 100-500 lines | Complete class with methods |
| Interface | 10-100 lines | Interface definitions |
| Model/Entity | 20-200 lines | Data models, POCOs |
| Service | 150-600 lines | Business logic services |
| Controller | 100-400 lines | API controllers |
| Test Class | 100-800 lines | Unit test suites |
Example Violation:
// ❌ BAD: 800-line "God Class"
public class UserManager
{
// 100+ lines: User CRUD
// 100+ lines: Authentication
// 100+ lines: Authorization
// 100+ lines: Email notifications
// 100+ lines: Audit logging
// 100+ lines: Report generation
// 200+ lines: Business rules
}Recommended Fix:
// ✅ GOOD: Separated into focused classes
public class UserRepository { /* 150 lines: CRUD only */ }
public class AuthenticationService { /* 120 lines: Auth only */ }
public class AuthorizationService { /* 100 lines: Authz only */ }
public class NotificationService { /* 80 lines: Notifications */ }
public class AuditLogger { /* 60 lines: Logging */ }
public class UserReportGenerator { /* 120 lines: Reports */ }
public class UserBusinessRules { /* 200 lines: Business logic */ }C# Complexity Sources:
- if/else statements
- switch/case statements
- while/do-while loops
- for/foreach loops
- catch blocks
- ternary operators (?:)
- null-coalescing operators (??, ??=)
- LINQ query expressions
- Pattern matching
Thresholds:
| Class Type | Complexity Threshold |
|---|---|
| Model/POCO | 3 |
| Repository | 8 |
| Service | 12 |
| Controller | 10 |
| Utility | 6 |
Example Violation:
// ❌ BAD: Complexity = 18
public decimal CalculatePrice(Order order, Customer customer)
{
decimal price = 0;
if (order != null)
{
if (order.Items != null && order.Items.Any())
{
foreach (var item in order.Items)
{
if (item.Product != null)
{
price += item.Product.Price * item.Quantity;
if (item.Product.IsOnSale)
{
if (customer.IsPremium)
{
price -= price * 0.25m;
}
else if (customer.YearsActive > 5)
{
price -= price * 0.20m;
}
else if (customer.YearsActive > 2)
{
price -= price * 0.15m;
}
else
{
price -= price * 0.10m;
}
}
}
}
}
if (order.Total > 100)
{
price -= 10;
}
}
return price;
}Recommended Fix:
// ✅ GOOD: Complexity = 4 per method
public decimal CalculatePrice(Order order, Customer customer)
{
if (order?.Items == null || !order.Items.Any())
return 0;
var subtotal = order.Items.Sum(item => CalculateItemPrice(item, customer));
var discount = CalculateOrderDiscount(order);
return Math.Max(0, subtotal - discount);
}
private decimal CalculateItemPrice(OrderItem item, Customer customer)
{
if (item?.Product == null) return 0;
var basePrice = item.Product.Price * item.Quantity;
var discount = item.Product.IsOnSale
? GetSaleDiscount(customer)
: 0;
return basePrice * (1 - discount);
}
private decimal GetSaleDiscount(Customer customer)
{
if (customer.IsPremium) return 0.25m;
if (customer.YearsActive > 5) return 0.20m;
if (customer.YearsActive > 2) return 0.15m;
return 0.10m;
}
private decimal CalculateOrderDiscount(Order order)
{
return order.Total > 100 ? 10 : 0;
}Rule ID: interface-segregation
Category: SOLID Principles
Severity: Warning
Autofix: Not available
Description:
Interfaces should be focused and not force implementations to depend on methods they don't use.
Example Violation:
// ❌ BAD: Fat interface
public interface IUserService
{
void CreateUser(User user);
void DeleteUser(int id);
void UpdateUser(User user);
User GetUser(int id);
List<User> GetAllUsers();
void SendWelcomeEmail(int userId);
void GenerateUserReport(int userId);
void ExportUsersToExcel();
void ImportUsersFromCsv(string path);
void CalculateUserStatistics();
}Recommended Fix:
// ✅ GOOD: Segregated interfaces
public interface IUserRepository
{
void Create(User user);
void Delete(int id);
void Update(User user);
User GetById(int id);
IEnumerable<User> GetAll();
}
public interface IUserNotificationService
{
void SendWelcomeEmail(int userId);
}
public interface IUserReportService
{
void GenerateReport(int userId);
void ExportToExcel();
}
public interface IUserImportService
{
void ImportFromCsv(string path);
}
public interface IUserAnalyticsService
{
UserStatistics CalculateStatistics();
}Rule ID: async-naming-convention
Category: Naming
Severity: Informational
Autofix: Available
Description:
Async methods should be suffixed with "Async" to indicate asynchronous behavior.
Example Violation:
// ❌ BAD: Missing Async suffix
public async Task<User> GetUser(int id)
{
return await _repository.FindAsync(id);
}Recommended Fix:
// ✅ GOOD: Async suffix
public async Task<User> GetUserAsync(int id)
{
return await _repository.FindAsync(id);
}Rule ID: dispose-pattern
Category: Resource Management
Severity: Error
Autofix: Not available
Description:
Classes managing unmanaged resources should implement IDisposable pattern correctly.
Example Violation:
// ❌ BAD: Missing disposal
public class FileLogger
{
private StreamWriter _writer;
public FileLogger(string path)
{
_writer = new StreamWriter(path);
}
public void Log(string message)
{
_writer.WriteLine(message);
}
// _writer is never disposed!
}Recommended Fix:
// ✅ GOOD: Proper IDisposable pattern
public class FileLogger : IDisposable
{
private StreamWriter _writer;
private bool _disposed;
public FileLogger(string path)
{
_writer = new StreamWriter(path);
}
public void Log(string message)
{
if (_disposed)
throw new ObjectDisposedException(nameof(FileLogger));
_writer.WriteLine(message);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_writer?.Dispose();
}
_disposed = true;
}
}
~FileLogger()
{
Dispose(false);
}
}
// Or use using statement:
public class SimpleLogger : IDisposable
{
private readonly StreamWriter _writer;
public SimpleLogger(string path)
{
_writer = new StreamWriter(path);
}
public void Dispose()
{
_writer?.Dispose();
}
}Rule ID: null-reference-safety
Category: Safety
Severity: Warning
Autofix: Partial
Description:
Use nullable reference types and null-conditional operators to prevent NullReferenceException.
Example Violation:
// ❌ BAD: Potential null reference
public string GetUserEmail(User user)
{
return user.Email.ToLower(); // May throw if user or Email is null
}Recommended Fix:
// ✅ GOOD: Null-safe access
public string? GetUserEmail(User? user)
{
return user?.Email?.ToLower();
}
// Or with explicit checks:
public string GetUserEmail(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
if (string.IsNullOrEmpty(user.Email))
throw new InvalidOperationException("User email is not set");
return user.Email.ToLower();
}{
"architectural-discipline": {
"languages": {
"csharp": {
"maxLines": 400,
"maxComplexity": 12,
"maxLinesPerFunction": 60,
"enforceAsyncNaming": true,
"enforceDisposablePattern": true
}
}
}
}ADP helps enforce SOLID principles:
- Single Responsibility - Classes should have one reason to change
- Open/Closed - Open for extension, closed for modification
- Liskov Substitution - Derived classes must be substitutable for base classes
- Interface Segregation - Many specific interfaces better than one general
- Dependency Inversion - Depend on abstractions, not concretions