# Component Design Standards for C#
This document outlines the component design standards for C# development, focusing on creating reusable, maintainable, and scalable components. These standards aim to promote consistency, readability, and adherence to best practices while leveraging modern C# features.
## 1. Principles of Component Design
### 1.1. Single Responsibility Principle (SRP)
A component should have one, and only one, reason to change.
* **Do This:** Design components that encapsulate a single, well-defined responsibility.
* **Don't Do This:** Create "god classes" or components with multiple unrelated responsibilities.
**Why SRP Matters:** Improves maintainability, testability, and reduces the impact of changes. A component focused on a single task is easier to understand, modify, and test.
**Code Example:**
"""csharp
// Good: Separate concerns
public interface IOrderProcessor
{
void ProcessOrder(Order order);
}
public interface IOrderValidator
{
bool ValidateOrder(Order order);
}
public class OrderProcessor : IOrderProcessor
{
private readonly IOrderValidator _orderValidator;
private readonly IPaymentService _paymentService;
public OrderProcessor(IOrderValidator orderValidator, IPaymentService paymentService)
{
_orderValidator = orderValidator;
_paymentService = paymentService;
}
public void ProcessOrder(Order order)
{
if (!_orderValidator.ValidateOrder(order))
{
throw new InvalidOperationException("Order is invalid.");
}
_paymentService.ProcessPayment(order.PaymentInfo);
// ... other order processing logic
}
}
// Bad: Violates SRP
public class OrderService // Contains multiple responsibilities (validation, processing, payment)
{
public void ProcessOrder(Order order)
{
if (!IsValidOrder(order))
{
// ... validation logic
}
// ... payment logic
// ... order processing logic
}
private bool IsValidOrder(Order order)
{
// ... order validation logic
return true; // or false
}
}
"""
### 1.2. Open/Closed Principle (OCP)
Components should be open for extension but closed for modification.
* **Do This:** Use inheritance, interfaces, and composition to extend functionality without altering the original component.
* **Don't Do This:** Modify existing component code to add new features, as this can introduce regressions and break existing functionality.
**Why OCP Matters:** Enhances code reuse, reduces the risk of introducing bugs when adding new features, and simplifies maintenance.
**Code Example:**
"""csharp
// Good: Using an abstract base class
public abstract class DiscountCalculator
{
public abstract decimal CalculateDiscount(decimal price);
}
public class SummerDiscountCalculator : DiscountCalculator
{
public override decimal CalculateDiscount(decimal price)
{
return price * 0.10m; // 10% discount
}
}
public class WinterDiscountCalculator : DiscountCalculator
{
public override decimal CalculateDiscount(decimal price)
{
return price * 0.15m; // 15% discount
}
}
// Bad: Modifying existing class
public class DiscountCalculator
{
public decimal CalculateDiscount(decimal price, string season)
{
if (season == "Summer")
{
return price * 0.10m;
}
else if (season == "Winter")
{
return price * 0.15m;
}
return price;
}
}
"""
### 1.3. Liskov Substitution Principle (LSP)
Subtypes should be substitutable for their base types without altering the correctness of the program.
* **Do This:** Ensure that derived classes behave consistently with their base classes.
* **Don't Do This:** Override methods in a way that changes the expected behavior or violates the base class contract.
**Why LSP Matters:** Guarantees that polymorphism works as expected, promoting reliable code and reducing unexpected errors.
**Code Example:**
"""csharp
// Good: Maintaining behavior consistency
public abstract class Shape
{
public abstract int Area();
}
public class Rectangle : Shape
{
public int Width { get; set; }
public int Height { get; set; }
public override int Area() => Width * Height;
}
public class Square : Shape
{
public int Side { get; set; }
public override int Area() => Side * Side;
}
// Bad: Violating the Rectangle/Square relationship (LSP)
public class BadRectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int Area() => Width * Height;
}
public class BadSquare : BadRectangle
{
// Violates LSP - setting width changes height and vice-versa
private int _side;
public override int Width
{
get => _side;
set { _side = value; Height = value; }
}
public override int Height
{
get => _side;
set { _side = value; Width = value; }
}
}
"""
### 1.4. Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
* **Do This:** Create small, specific interfaces that cater to specific client needs.
* **Don't Do This:** Create large, monolithic interfaces requiring clients to implement methods they don't need.
**Why ISP Matters:** Reduces coupling, improves code clarity and maintainability, and allows for more flexible component design.
**Code Example:**
"""csharp
// Good: Segregated Interfaces
public interface IPrintable
{
void Print();
}
public interface IScannable
{
void Scan();
}
public class ModernPrinter : IPrintable, IScannable
{
public void Print() { /* Printing logic */ }
public void Scan() { /* Scanning logic */ }
}
public class SimplePrinter : IPrintable
{
public void Print() { /* Printing logic */ }
}
// Bad: Fat Interface
public interface IMultiFunctionDevice
{
void Print();
void Scan();
void Fax();
}
"""
### 1.5. Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
* **Do This:** Use dependency injection (DI) to inject dependencies through constructor injection, property injection, or method injection. Rely on Interfaces and abstract classes instead of concrete implementations.
* **Don't Do This:** Create tightly coupled dependencies between high-level and low-level modules. Use "new" keyword liberally throughout your code.
**Why DIP Matters:** Promotes loose coupling, improves testability, and makes code more flexible and reusable.
**Code Example:**
"""csharp
// Good: Using Dependency Injection
public interface IEmailService
{
void SendEmail(string to, string subject, string body);
}
public class EmailService : IEmailService
{
public void SendEmail(string to, string subject, string body)
{
// ... email sending logic
}
}
public class NotificationService
{
private readonly IEmailService _emailService;
public NotificationService(IEmailService emailService) // Constructor Injection
{
_emailService = emailService;
}
public void SendNotification(string userEmail, string message)
{
_emailService.SendEmail(userEmail, "Notification", message);
}
}
// Usage (e.g., in a DI container):
// services.AddScoped();
// services.AddScoped();
// Bad: Tight coupling
public class BadNotificationService
{
private readonly EmailService _emailService = new EmailService(); // Tight coupling
public void SendNotification(string userEmail, string message)
{
_emailService.SendEmail(userEmail, "Notification", message);
}
}
"""
## 2. Component Design Patterns
### 2.1. Factory Pattern
Use factory patterns to abstract the creation of objects.
* **Do This:** Employ factory methods or abstract factories to decouple the creation logic from the client code.
* **Don't Do This:** Directly instantiate objects within client code, especially when object creation is complex.
**Why Factory Pattern Matters:** Provides flexibility in object creation, simplifies testing, and adheres to the open/closed principle.
**Code Example:**
"""csharp
// Abstract Product
public abstract class Product
{
public abstract string Operation();
}
// Concrete Products
public class ConcreteProductA : Product
{
public override string Operation() => "ConcreteProductA";
}
public class ConcreteProductB : Product
{
public override string Operation() => "ConcreteProductB";
}
// Abstract Factory
public abstract class Factory
{
public abstract Product CreateProduct();
}
// Concrete Factories
public class ConcreteFactoryA : Factory
{
public override Product CreateProduct() => new ConcreteProductA();
}
public class ConcreteFactoryB : Factory
{
public override Product CreateProduct() => new ConcreteProductB();
}
// Client code
public class Client
{
private readonly Product _product;
public Client(Factory factory)
{
_product = factory.CreateProduct();
}
public string Run() => _product.Operation();
}
"""
### 2.2. Strategy Pattern
Define a family of algorithms, encapsulate each one, and make them interchangeable.
* **Do This:** Implement different algorithms as separate classes and use composition to switch between them.
* **Don't Do This:** Use conditional statements to select algorithms within a single component.
**Why Strategy Pattern Matters:** Promotes flexibility, reduces code duplication, and allows for easy addition of new algorithms.
**Code Example:**
"""csharp
// Strategy Interface
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
// Concrete Strategies
public class CreditCardPayment : IPaymentStrategy
{
private readonly string _cardNumber;
private readonly string _expiryDate;
public CreditCardPayment(string cardNumber, string expiryDate)
{
_cardNumber = cardNumber;
_expiryDate = expiryDate;
}
public void Pay(decimal amount)
{
// ... credit card payment logic
Console.WriteLine($"Paid {amount} using Credit Card: {_cardNumber}");
}
}
public class PayPalPayment : IPaymentStrategy
{
private readonly string _email;
public PayPalPayment(string email)
{
_email = email;
}
public void Pay(decimal amount)
{
// ... PayPal payment logic
Console.WriteLine($"Paid {amount} using PayPal: {_email}");
}
}
// Context
public class ShoppingCart
{
private IPaymentStrategy _paymentStrategy;
public void SetPaymentStrategy(IPaymentStrategy paymentStrategy)
{
_paymentStrategy = paymentStrategy;
}
public void Checkout(decimal amount)
{
_paymentStrategy.Pay(amount);
}
}
// Usage
// var cart = new ShoppingCart();
// cart.SetPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456", "12/25"));
// cart.Checkout(100.00m);
"""
### 2.3. Observer Pattern
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
* **Do This:** Use the "IObservable" and "IObserver" interfaces or events and delegates to implement the observer pattern.
* **Don't Do This:** Create tight coupling between subjects and observers.
**Why Observer Pattern Matters:** Decouples subjects and observers, allows for flexible event handling, and facilitates reactive programming.
**Code Example:**
"""csharp
// Subject Interface
public interface IObservable
{
void Subscribe(IObserver observer);
void Unsubscribe(IObserver observer);
}
// Observer Interface
public interface IObserver
{
void Update(string message);
}
// Concrete Subject
public class NewsAgency : IObservable
{
private readonly List _observers = new List();
private string _news;
public void Subscribe(IObserver observer)
{
_observers.Add(observer);
}
public void Unsubscribe(IObserver observer)
{
_observers.Remove(observer);
}
public string News
{
get => _news;
set
{
_news = value;
NotifyObservers();
}
}
private void NotifyObservers()
{
foreach (var observer in _observers)
{
observer.Update(_news);
}
}
}
// Concrete Observers
public class NewsReader : IObserver
{
private readonly string _name;
public NewsReader(string name)
{
_name = name;
}
public void Update(string message)
{
Console.WriteLine($"{_name} received news: {message}");
}
}
"""
## 3. Modern C# Features for Component Design
### 3.1. Records
Use records for immutable data transfer objects (DTOs) and value objects.
* **Do This:** Define records with properties, deconstruction, and equality implementations.
* **Don't Do This:** Use classes for simple data holders, as records provide more concise syntax and built-in immutability.
**Why Records Matter:** Simpler syntax, improved immutability, and enhanced data integrity.
**Code Example:**
"""csharp
public record Person(string FirstName, string LastName, int Age);
// Usage
Person person = new("John", "Doe", 30);
Console.WriteLine(person); // Output: Person { FirstName = John, LastName = Doe, Age = 30 }
// Deconstruction
var (firstName, lastName, age) = person;
Console.WriteLine($"First Name: {firstName}, Last Name: {lastName}, Age: {age}");
"""
### 3.2. Nullable Reference Types
Enable nullable reference types to avoid null reference exceptions.
* **Do This:** Annotate reference types with "?" to indicate they can be null. Handle nullable values properly with null checks or the null-conditional operator ("?.").
* **Don't Do This:** Ignore nullable warnings, as they indicate potential null reference issues.
**Why Nullable Reference Types Matter:** Helps prevent null reference exceptions, improves code safety, and enhances code analysis.
**Code Example:**
"""csharp
#nullable enable
public class Address
{
public string? Street { get; set; } // Street can be null
public string City { get; set; } = ""; // City cannot be null
}
public class Person
{
public string FirstName { get; set; } = "";
public Address? Address { get; set; } // Address can be null
public string GetFullAddress()
{
return Address?.Street + ", " + Address?.City; // Using null-conditional operator
}
}
#nullable disable
"""
### 3.3. Asynchronous Programming with "async" and "await"
Use asynchronous programming for I/O-bound and CPU-bound operations.
* **Do This:** Use "async" and "await" for long-running operations to prevent UI freezing and improve responsiveness.
* **Don't Do This:** Block threads with ".Result" or ".Wait()" when awaiting tasks.
**Why Async/Await Matters:** Improves application performance, enhances responsiveness, and provides a cleaner way to handle asynchronous operations.
**Code Example:**
"""csharp
public async Task DownloadDataAsync(string url)
{
using HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
return content;
}
// Proper usage:
// string data = await DownloadDataAsync("https://example.com");
// Anti-pattern, blocking the thread:
// string data = DownloadDataAsync("https://example.com").Result;
"""
### 3.4. Dependency Injection (DI) with .NET's Built-in Container
Utilize .NET's built-in DI container for managing component dependencies.
* **Do This:** Register services with lifetime scopes (singleton, scoped, transient) and inject dependencies through constructor injection. Use interfaces for service contracts and provide concrete implementations.
* **Don't Do This:** Manually create and manage dependencies within components, as this leads to tight coupling and poor testability.
**Why DI Matters:** Promotes loose coupling, improves testability, and simplifies component management.
**Code Example:**
"""csharp
// Startup.cs or Program.cs in .NET 6+
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient(); // Transient lifetime
services.AddScoped(); // Scoped lifetime
services.AddSingleton(); // Singleton lifetime
services.AddControllers();
}
public class MyComponent
{
private readonly IMyService _myService;
public MyComponent(IMyService myService)
{
_myService = myService;
}
public void DoSomething()
{
_myService.PerformAction();
}
}
"""
### 3.5. Top-Level Statements and Implicit Usings (C# 9+)
Use top-level statements to simplify console applications and scripts. Utilize implicit usings to reduce boilerplate code.
* **Do This:** Enable implicit usings in your project file ("enable"). Use top-level statements for simple console apps and scripts.
* **Don't Do This:** Rely on verbose boilerplate code when top-level statements and implicit usings can simplify your code.
**Why Top-Level Statements and Implicit Usings Matter:** Reduces boilerplate, improves code readability, and simplifies project setup.
**Code Example:**
"""csharp
// Program.cs (Top-level statements)
// enable in .csproj
Console.WriteLine("Hello, World!");
// You can directly use Console, HttpClient, etc. without explicit usings.
// Sample .csproj file:
//
//
// net7.0
// enable
// enable
//
//
"""
## 4. Component Design Best Practices
### 4.1. Favor Composition Over Inheritance
Prefer composition over inheritance to achieve code reuse and flexibility.
* **Do This:** Create components that compose behaviors from other smaller components.
* **Don't Do This:** Create deep inheritance hierarchies, as they can lead to the fragile base class problem and limit flexibility.
**Why Composition Matters:** Reduces coupling, enhances flexibility, and simplifies component evolution.
**Code Example:**
"""csharp
// Good: Using Composition
public interface IFlyable
{
void Fly();
}
public interface ISwimmable
{
void Swim();
}
public class CanFly : IFlyable
{
public void Fly() { Console.WriteLine("Flying..."); }
}
public class CanSwim : ISwimmable
{
public void Swim() { Console.WriteLine("Swimming..."); }
}
public class Duck
{
private readonly IFlyable _flyable;
private readonly ISwimmable _swimmable;
public Duck(IFlyable flyable, ISwimmable swimmable)
{
_flyable = flyable;
_swimmable = swimmable;
}
public void Fly() => _flyable.Fly();
public void Swim() => _swimmable.Swim();
}
// Bad: Using Inheritance (less flexible)
public class Animal
{
public virtual void Move() { }
}
public class Bird : Animal
{
public override void Move() { Console.WriteLine("Flying..."); }
}
"""
### 4.2. Implement Defensive Programming
Write code with built-in checks to prevent errors and handle unexpected conditions gracefully.
* **Do This:** Validate input parameters, handle exceptions appropriately, and use assertions to verify assumptions.
* **Don't Do This:** Assume that input is always valid or ignore potential error conditions.
**Why Defensive Programming Matters:** Improves code reliability, reduces the risk of runtime errors, and enhances application security.
**Code Example:**
"""csharp
public static int Divide(int numerator, int denominator)
{
if (denominator == 0)
{
throw new ArgumentException("Denominator cannot be zero.");
}
int result = numerator / denominator;
return result;
}
public static void ProcessData(string data)
{
if (string.IsNullOrEmpty(data))
{
throw new ArgumentException("Data cannot be null or empty.");
}
try
{
// ... processing logic
}
catch (Exception ex)
{
// Log the exception or take appropriate action
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
"""
### 4.3. Use Code Contracts
Use contracts to define preconditions, postconditions, and object invariants.
* **Do This:** Define contracts using "Contract.Requires", "Contract.Ensures", and "Contract.Invariant" from the "System.Diagnostics.Contracts" namespace.
* **Don't Do This:** Rely solely on comments to document contracts, as comments are not enforced at runtime.
**Why Code Contracts Matter:** Improves code reliability, enables static analysis, and enhances runtime validation.
**Code Example:**
"""csharp
using System.Diagnostics.Contracts;
public class Calculator
{
public int Add(int x, int y)
{
Contract.Requires(x >= 0);
Contract.Requires(y >= 0);
Contract.Ensures(Contract.Result() >= x);
Contract.Ensures(Contract.Result() >= y);
return x + y;
}
}
"""
### 4.4. Exception Handling
Handle exceptions correctly to prevent application crashes and provide informative error messages.
* **Do This:** Catch specific exception types, log exceptions, and re-throw exceptions when necessary (e.g., after logging or performing cleanup). Use custom exceptions derived from "System.Exception" or "System.ApplicationException" where appropriate.
* **Don't Do This:** Catch generic exceptions ("Exception"), as this can hide important error information or swallow exceptions unnecessarily. Swallow exceptions without logging or handling them.
**Why Exception Handling Matters:** Improves application stability, provides useful error information, and allows for graceful recovery from errors.
**Code Example:**
"""csharp
public class CustomException : Exception
{
public CustomException(string message) : base(message) {}
public CustomException(string message,Exception innerException) : base(message, innerException) {}
}
public void ProcessData(string data)
{
try
{
// ... processing logic
if (string.IsNullOrEmpty(data))
{
throw new CustomException("Data is invalid");
}
}
catch (ArgumentNullException ex)
{
// Log the exception
Console.Error.WriteLine($"ArgumentNullException: {ex.Message}");
// Re-throw the exception, or handle it gracefully
throw;
}
catch (CustomException ex)
{
Console.Error.WriteLine($"CustomException: {ex.Message}");
}
catch (Exception ex) //Catch more specific exceptions before the generic Exception
{
// Log the exception
Console.Error.WriteLine($"An unexpected error occurred: {ex.Message}");
// Consider re-throwing the exception or handling it gracefully.
}
}
"""
### 4.5. Design for Testability
Create components that are easy to test in isolation.
* **Do This:** Use dependency injection to inject mock dependencies, follow SRP, and avoid static dependencies.
* **Don't Do This:** Create tightly coupled components that are difficult to isolate and test.
**Why Testability Matters:** Improves code quality, reduces the risk of bugs, and simplifies refactoring.
**Code Example:**
"""csharp
// Designed for testability
public interface IDataService
{
string GetData();
}
public class DataService : IDataService
{
public string GetData()
{
// ... data retrieval logic
return "Real Data";
}
}
public class MyComponent
{
private readonly IDataService _dataService;
public MyComponent(IDataService dataService)
{
_dataService = dataService;
}
public string ProcessData()
{
string data = _dataService.GetData();
// ... processing logic
return data.ToUpper();
}
}
// Test Example (using Moq)
// var mockDataService = new Mock();
// mockDataService.Setup(x => x.GetData()).Returns("Mock Data");
// var component = new MyComponent(mockDataService.Object);
// Assert.AreEqual("MOCK DATA", component.ProcessData());
"""
By adhering to these component design standards, C# developers can create robust, maintainable, and scalable applications that meet the demands of modern software development. These guidelines promote best practices in component design by emphasizing loose coupling, high cohesion, and the strategic use of modern C# features.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Performance Optimization Standards for C# This document outlines performance optimization standards for C# development. It aims to guide developers in writing efficient and responsive applications by providing actionable guidelines, code examples, and explanations of underlying principles. These standards are designed to work with the latest versions of C# and the .NET ecosystem. ## 1. Architectural Considerations ### 1.1. Choose the Right Architecture * **Do This**: Select an architecture that aligns with the application's requirements and anticipated scale. Consider microservices, layered architecture, or event-driven architecture based on project needs. * **Don't Do This**: Blindly adopt an architecture without assessing its suitability for the project. Over-engineering or under-engineering can both lead to performance bottlenecks. * **Why**: Choosing the wrong architecture can result in poor scalability, increased complexity, and reduced performance. **Example:** """csharp // Microservice architecture example (High level concept) // Multiple services communicating via message queue (e.g. RabbitMQ, Azure Service Bus) // Order Service public class OrderService { private readonly IMessageQueue _messageQueue; public OrderService(IMessageQueue messageQueue) { _messageQueue = messageQueue; } public void PlaceOrder(Order order) { // Process order // ... _messageQueue.Publish("order.placed", order); } } """ ### 1.2. Asynchronous Programming * **Do This**: Utilize asynchronous programming ("async"/"await") for I/O-bound operations such as network requests, file access, and database queries. * **Don't Do This**: Block the main thread on I/O operations. This can lead to UI freezes and decreased responsiveness. * **Why**: Asynchronous operations free up threads to handle other tasks while waiting for I/O to complete. **Example:** """csharp // Good: Asynchronous network request public async Task<string> GetWebPageContentAsync(string url) { using HttpClient client = new HttpClient(); return await client.GetStringAsync(url); } // Bad: Synchronous network request (blocking) public string GetWebPageContent(string url) { using HttpClient client = new HttpClient(); return client.GetStringAsync(url).Result; // Avoid .Result and .Wait() as it blocks } """ * If CPU bound use "Task.Run" but understand the consequences of thread pool exhaustion. """csharp public async Task<int> CalculateAsync(int input) { return await Task.Run(() => LongRunningCalculation(input)); } """ ### 1.3. Caching Strategies * **Do This**: Implement caching at different levels (e.g., in-memory, distributed cache) to reduce database load and improve response times. * **Don't Do This**: Overuse caching or cache data that changes frequently. Incorrect cache invalidation can lead to stale data. * **Why**: Caching stores frequently accessed data in a faster medium, reducing the need to repeatedly fetch it from slower sources. **Example:** """csharp // In-memory caching using MemoryCache using Microsoft.Extensions.Caching.Memory; public class ProductService { private readonly IMemoryCache _cache; public ProductService(IMemoryCache cache) { _cache = cache; } public Product GetProduct(int id) { return _cache.GetOrCreate(id, entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(5); // Fetch product from database return FetchProductFromDatabase(id); }); } private Product FetchProductFromDatabase(int id) { // Database logic return new Product { Id = id, Name = "Example Product" }; } } """ ### 1.4. Load Balancing * **Do This**: Distribute incoming traffic across multiple servers using a load balancer. * **Don't Do This**: Rely on a single server to handle all the load. This creates a single point of failure and limits scalability. * **Why**: Load balancing improves availability and performance by ensuring no single server is overwhelmed. ## 2. Code-Level Optimizations ### 2.1. String Manipulation * **Do This**: Use "StringBuilder" for concatenating strings in loops or when performing multiple modifications. Use "string.Create" when constructing immutable strings from spans. Use "InterpolatedStringHandler" for optimizing string interpolation scenarios. * **Don't Do This**: Use the "+" operator for repeatedly concatenating strings within loops. This creates multiple string instances, leading to performance overhead. * **Why**: "StringBuilder" is mutable and avoids creating numerous temporary string objects. **Example:** """csharp // Good: Using StringBuilder using System.Text; public string BuildString(string[] parts) { StringBuilder sb = new StringBuilder(); foreach (string part in parts) { sb.Append(part); } return sb.ToString(); } // Bad: Using + operator inefficiently public string ConcatenateStrings(string[] parts) { string result = ""; foreach (string part in parts) { result += part; //Inneficient string operations. } return result; } // Better: Using String.Create public static string CreateStringFromChars(ReadOnlySpan<char> charArray) { return string.Create(charArray.Length, charArray, (span, state) => { state.CopyTo(span); }); } // Best: InterpolatedStringHandler - Custom handler for optimized interpolation using System.Runtime.CompilerServices; [InterpolatedStringHandler] public ref struct ImportantMessageInterpolationHandler { private StringBuilder _builder; private bool _enabled; public ImportantMessageInterpolationHandler(int literalLength, int formattedCount, out bool isEnabled) { _enabled = DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 17; // Example condition isEnabled = _enabled; if (_enabled) { _builder = new StringBuilder(literalLength + formattedCount * 10); // Adjust capacity as needed } else { _builder = null; } } public void AppendLiteral(string literal) { if (_enabled) { _builder.Append(literal); } } public void AppendFormatted(string value) { if (_enabled) { _builder.Append(value); } } public void AppendFormatted(int value) { if (_enabled) { _builder.Append(value); } } internal string GetFormattedText() => _builder?.ToString(); } public static class Logger { public static void LogImportantMessage(ImportantMessageInterpolationHandler message) { string formattedMessage = message.GetFormattedText(); if (!string.IsNullOrEmpty(formattedMessage)) { Console.WriteLine($"Important: {formattedMessage}"); } } } // Usage string name = "Alice"; int age = 30; Logger.LogImportantMessage($"User {name} is {age} years old."); //Optimized due to handler """ ### 2.2. Boxing and Unboxing * **Do This**: Avoid unnecessary boxing and unboxing by using generics and strongly typed collections. * **Don't Do This**: Store value types (e.g., "int", "bool", "struct") in non-generic collections like "ArrayList". * **Why**: Boxing creates a heap-allocated object for value types, leading to performance overhead. Unboxing retrieves the value from the heap object. **Example:** """csharp // Good: Using generic List List<int> numbers = new List<int>(); numbers.Add(1); // No boxing // Bad: Using ArrayList System.Collections.ArrayList values = new System.Collections.ArrayList(); values.Add(1); // Boxing occurs """ ### 2.3. LINQ Usage * **Do This**: Use LINQ judiciously, especially for small to medium-sized collections. Consider the performance implications of complex LINQ queries. Use struct enumerators when available. * **Don't Do This**: Overuse LINQ for simple operations or when performance is critical. Use imperative code for optimized performance when necessary. Be mindful of deferred execution. * **Why**: LINQ can be convenient, but its deferred execution and overhead can impact performance in certain scenarios. **Example:** """csharp // Good: Using LINQ for a small collection List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; var evenNumbers = numbers.Where(n => n % 2 == 0).ToList(); // When needing to optimize where LINQ is not efficient // Use struct enumerators for zero allocations public readonly struct MyCustomCollection { private readonly int[] _data; public MyCustomCollection(int[] data) => _data = data; public Enumerator GetEnumerator() => new Enumerator(_data); public struct Enumerator //Struct Enumerator, Value Type, no boxing { private readonly int[] _data; private int _index; internal Enumerator(int[] data) { _data = data; _index = -1; } public bool MoveNext() => ++_index < _data.Length; public int Current => _data[_index]; } } """ ### 2.4. Reflection * **Do This**: Minimize the use of reflection, as it is slower than direct code execution. Cache reflection results when possible. * **Don't Do This**: Use reflection excessively in performance-critical sections of code. * **Why**: Reflection involves runtime type discovery and dynamic method invocation, which are computationally expensive. **Example:** """csharp // Good: Caching reflection results using System.Reflection; public class ReflectionExample { private static readonly MethodInfo MyMethodInfo = typeof(MyClass).GetMethod("MyMethod"); public void CallMyMethod(MyClass obj) { MyMethodInfo.Invoke(obj, null); // Invoke cached method directly } } public class MyClass { public void MyMethod() { // Method implementation } } """ ### 2.5. Value Types vs. Reference Types * **Do This**: Use value types ("struct") for small, immutable data structures with value semantics to avoid heap allocations. Use "readonly struct" for maximum immutability and performance. * **Don't Do This**: Use value types for large or mutable data structures that are frequently passed around. * **Why**: Value types are allocated on the stack, reducing garbage collection pressure. **Example:** """csharp // Good: ReadOnly struct for immutable data public readonly struct Point { public readonly int X; public readonly int Y; public Point(int x, int y) { X = x; Y = y; } } """ ### 2.6. Dispose Resources * **Do This:** Implement the "IDisposable" interface for classes that manage unmanaged resources (e.g., file handles, network connections). Use "using" statements or try-finally blocks to ensure resources are disposed of properly. * **Don't Do This:** Neglect to dispose of resources, leading to memory leaks and performance degradation. * **Why**: Improper resource management can exhaust system resources and negatively impact application performance. **Example:** """csharp // Good: Using 'using' statement for resource disposal public void ProcessFile(string filePath) { using (StreamReader reader = new StreamReader(filePath)) { string content = reader.ReadToEnd(); // Process file content } // StreamReader is disposed here } //Good: Using IAsyncDisposable with asynchronous operations public class MyAsyncDisposable : IAsyncDisposable { private HttpClient _httpClient; public MyAsyncDisposable() { _httpClient = new HttpClient(); } public async ValueTask DisposeAsync() { if (_httpClient != null) { _httpClient.Dispose(); _httpClient = null; } await Task.CompletedTask; // Placeholder for any async cleanup } } public async Task UseAsyncDisposable() { await using (var myObject = new MyAsyncDisposable()) { //Use the async disposable object. } // DisposeAsync called automatically once outside scope. } """ ### 2.7. Collections * **Do This:** Choose the appropriate collection type based on usage patterns. Use "List<T>" for general-purpose collections, "Dictionary<TKey, TValue>" for key-value lookups, "HashSet<T>" for unique value storage, and appropriate concurrent collections for multithreaded contexts. Consider using "ImmutableArray<T>" and other immutable collections available from "System.Collections.Immutable" when dealing with multi-threaded scenarios and to reduce allocation. * **Don't Do This:** Use inappropriate collections that lead to performance bottlenecks (e.g., using "LinkedList<T>" for frequent random access). * **Why**: Different collections have different performance characteristics for insertion, deletion, retrieval, and iteration. **Example:** """csharp // Good: Using Dictionary for efficient key-value lookup Dictionary<string, int> ages = new Dictionary<string, int>(); ages.Add("Alice", 30); int aliceAge = ages["Alice"]; // Fast lookup //Good: Immutable Array used to manage a value in a multi-threaded environment. using System.Collections.Immutable; public class DataProcessor { private ImmutableArray<string> _data; public DataProcessor(string[] initialData) { _data = initialData.ToImmutableArray(); } public void AddData(string newData) { ImmutableInterlocked.Update(ref _data, oldData => oldData.Add(newData)); } public ImmutableArray<string> GetData() { return _data; } } //Bad: Accessing values by index on a LinkedList. LinkedList<string> list = new LinkedList<string>(); list.AddLast("one"); list.AddLast("two"); string value = list.ElementAt(1); // Very slow, O(n) """ ### 2.8. Memory Management * **Do This**: Be mindful of memory allocations, especially in loops and performance-critical sections. Use object pooling for frequently created and destroyed objects. Utilize "ArrayPool<T>" to minimize memory allocations for arrays. Explore "Memory<T>" and "Span<T>" for efficient memory access and manipulation. * **Don't Do This**: Create excessive temporary objects, leading to increased garbage collection overhead. * **Why**: Reducing memory allocations improves performance and reduces the load on the garbage collector. **Example:** """csharp // Good: Using ArrayPool using System.Buffers; public class ArrayPoolExample { public void ProcessData() { int[] array = ArrayPool<int>.Shared.Rent(1024); // Rent array from pool try { // Use array for (int i = 0; i < array.Length; i++) { array[i] = i; } } finally { ArrayPool<int>.Shared.Return(array); // Return array to pool } } } //Good: Using Memory<T> and Span<T> for processing data. public static class SpanExample { public static void ProcessBytes(Memory<byte> memory) { Span<byte> bytes = memory.Span; for (int i = 0; i < bytes.Length; i++) { bytes[i] = (byte)(bytes[i] * 2); // Example operation } } } """ ### 2.9. Regular Expressions * **Do This**: Use regular expressions efficiently. Compile regular expressions for reuse, and avoid complex patterns when simpler alternatives are available. * **Don't Do This**: Use overly complex regular expressions or create them repeatedly. This can lead to performance bottlenecks. * **Why**: Regular expression matching can be computationally expensive, especially for complex patterns. **Example:** """csharp // Good: Compiled regular expression using System.Text.RegularExpressions; public class RegexExample { private static readonly Regex MyRegex = new Regex(@"\d+", RegexOptions.Compiled); public bool IsMatch(string input) { return MyRegex.IsMatch(input); // Use compiled regex } } """ ## 3. Concurrency and Parallelism ### 3.1. Thread Pool * **Do This**: Utilize the thread pool for short-lived, CPU-bound operations. Avoid queueing long-running or blocking tasks to the thread pool. * **Don't Do This**: Starve the thread pool by queueing too many long-running tasks. This can lead to performance degradation. * **Why**: The thread pool manages a set of threads to execute tasks efficiently. **Example:** """csharp // Good: Using thread pool using System.Threading.Tasks; public class ThreadPoolExample { public void ProcessData(int data) { Task.Run(() => { // Process data on thread pool Console.WriteLine($"Processing {data} on thread {Thread.CurrentThread.ManagedThreadId}"); }); } } """ ### 3.2. Task Parallel Library (TPL) * **Do This**: Use TPL ("Task", "Parallel") for parallelizing CPU-bound operations. Use "async"/"await" for I/O-bound operations. Be mindful of thread synchronization issues when accessing shared resources. * **Don't Do This**: Overuse parallelism or introduce unnecessary complexity with TPL. Misuse of TPL can lead to deadlocks and race conditions. * **Why**: TPL simplifies parallel programming and allows leveraging multi-core processors. **Example:** """csharp // Good: Using Parallel.For public void ProcessDataParallel(int[] data) { Parallel.For(0, data.Length, i => { // Process data[i] in parallel data[i] = data[i] * 2; Console.WriteLine($"Processing {data[i]} on thread {Thread.CurrentThread.ManagedThreadId}"); }); } """ ### 3.3. Lock Contention * **Do This**: Minimize lock contention by using fine-grained locks or lock-free data structures. Consider using "ConcurrentDictionary<TKey, TValue>" and other concurrent collections. * **Don't Do This**: Use coarse-grained locks that serialize access to large sections of code. This can significantly reduce concurrency. * **Why**: Lock contention occurs when multiple threads try to acquire the same lock simultaneously, leading to performance bottlenecks. **Example:** """csharp // Good: Using ConcurrentDictionary using System.Collections.Concurrent; public class ConcurrentDictionaryExample { private readonly ConcurrentDictionary<string, int> _counts = new ConcurrentDictionary<string, int>(); public void IncrementCount(string key) { _counts.AddOrUpdate(key, 1, (k, v) => v + 1); } public int GetCount(string key) { _counts.TryGetValue(key, out int count); return count; } } """ ## 4. Data Access ### 4.1. Entity Framework Core (EF Core) * **Do This**: Use EF Core efficiently by minimizing round trips to the database. Use projections ("Select") to retrieve only the necessary columns. Use compiled queries for frequently executed queries. Use "AsNoTracking()" when change tracking is not required. * **Don't Do This**: Retrieve entire entities when only a few properties are needed. Perform unnecessary database round trips. Neglect to use compiled queries. * **Why**: Reducing database round trips improves performance and reduces database load. **Example:** """csharp // Good: Using projection to retrieve only necessary columns using Microsoft.EntityFrameworkCore; public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("YourConnectionString"); } } public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } public class DataAccessExample { public string GetProductName(int productId) { using ProductContext context = new ProductContext(); return context.Products .Where(p => p.Id == productId) .Select(p => p.Name) .AsNoTracking() .FirstOrDefault(); } } // Good: Compiled Queries public class DataAccessExample { /* Define a compiled query using EF.CompileQuery */ private static readonly Func<ProductContext, int, Product> _getProductById = EF.CompileQuery((ProductContext context, int productId) => context.Products .FirstOrDefault(e => e.Id == productId)); public Product GetProductById(int productId) { using (var context = new ProductContext()) { /* Call the compiled query, passing in the context */ Product product = _getProductById(context, productId); return product; } } } """ ### 4.2. Dapper * **Do This:** Consider using Dapper, a micro-ORM, for simpler and faster data access, especially for read-only scenarios or when full ORM features are not needed. * **Don't Do This:** Use Dapper when a full ORM like EF Core is required for complex object mapping and change tracking. * **Why:** Dapper offers a lightweight alternative to full ORMs, providing faster query execution and reduced overhead. **Example:** """csharp // Good: Using Dapper using Dapper; using System.Data.SqlClient; public class DapperExample { public Product GetProduct(int productId) { using SqlConnection connection = new SqlConnection("YourConnectionString"); connection.Open(); return connection.QueryFirstOrDefault<Product>("SELECT * FROM Products WHERE Id = @Id", new { Id = productId }); } } """ ### 4.3. Connection Pooling * **Do This**: Utilize connection pooling to reuse database connections and reduce connection overhead. Ensure connections are properly closed and disposed of. * **Don't Do This**: Create new database connections for every operation. This results in significant performance overhead. * **Why**: Connection pooling maintains a pool of open connections that can be reused, avoiding the cost of repeatedly opening and closing connections. ## 5. Monitoring and Profiling ### 5.1. Performance Counters * **Do This**: Use performance counters to monitor resource usage (e.g., CPU, memory, disk I/O) of the application. * **Don't Do This**: Neglect to monitor application performance. This makes it difficult to identify performance bottlenecks. * **Why**: Performance counters provide valuable insights into application resource utilization. ### 5.2. Profiling Tools * **Do This**: Use profiling tools (e.g., Visual Studio Profiler, dotTrace, PerfView) to identify performance bottlenecks and optimize code execution. * **Don't Do This**: Guess at performance issues without profiling. This can lead to wasted effort and ineffective optimizations. * **Why**: Profiling tools provide detailed information about code execution, helping to pinpoint performance bottlenecks. ### 5.3. Logging * **Do This**: Implement structured logging to track application events and performance metrics. Use appropriate logging levels (e.g., "Debug", "Information", "Warning", "Error") to avoid excessive logging in production. * **Don't Do This**: Log excessively or store sensitive data in logs. This can impact performance and security. * **Why**: Logging provides valuable information for debugging and performance analysis. ## 6. Specific C# Feature Optimizations ### 6.1 Span\<T> and Memory\<T> * **Do This**: Utilize "Span<T>" and "Memory<T>" for efficient memory access and manipulation, especially when dealing with arrays, strings, and streams. These types provide a way to work with contiguous regions of memory without unnecessary copying. * **Don't Do This**: Rely on traditional array indexing and string manipulation methods in performance-critical sections where "Span<T>" and "Memory<T>" can offer significant performance improvements. * **Why**: "Span<T>" and "Memory<T>" avoid heap allocations and provide direct access to memory, improving performance and reducing garbage collection pressure. **Example:** """csharp // Good: Using Span<T> for processing bytes public static class SpanExample { public static void ProcessBytes(Span<byte> bytes) { for (int i = 0; i < bytes.Length; i++) { bytes[i] = (byte)(bytes[i] * 2); // Example operation } } public static void ExampleUsage(byte[] data) { ProcessBytes(data.AsSpan()); } } """ ### 6.2. Records * **Do This:** Use records, especially "readonly record struct", to represent immutable data structures with value equality, resulting in less code than equivalent classes and value types. * **Don't Do This:** Overuse records where mutable classes might be more appropriate or where value equality is not needed. * **Why:** Records provide concise syntax for creating immutable data structures with value equality, improving code readability and potentially performance due to immutability. **Example:** """csharp // Good: Using readonly record struct for immutable data public readonly record struct Point(int X, int Y); """ ### 6.3. Struct Enumerators and Iterators * **Do This**: To optimise 'foreach' loops, use struct enumerators when dealing with collection types. * **Don't Do This**: Use Enumerable/IEnumerator (reference type) enumerators when a struct enumerator will do, causing unnecessary boxing. **Example:** """csharp // Good: Using struct enumerators when dealing with collection types public readonly struct MyCustomCollection { private readonly int[] _data; public MyCustomCollection(int[] data) => _data = data; public Enumerator GetEnumerator() => new Enumerator(_data); public struct Enumerator //Struct Enumerator, Value Type, no boxing { private readonly int[] _data; private int _index; internal Enumerator(int[] data) { _data = data; _index = -1; } public bool MoveNext() => ++_index < _data.Length; public int Current => _data[_index]; } } """ By following these performance optimization standards, C# developers can create efficient, responsive, and scalable applications that deliver optimal performance and user experience. This document should be treated as a living document and updated periodically to reflect the latest C# features and best practices.
# Core Architecture Standards for C# This document outlines the core architecture standards for C# development, focusing on fundamental architectural patterns, project structure, and organizational principles specific to C#. It targets modern C# approaches, emphasizing maintainability, performance, and security. ## 1. Architectural Patterns ### 1.1. Layered Architecture **Standard:** Implement a layered architecture to separate concerns and improve maintainability. **Do This:** * Define clear layers: Presentation, Application, Domain, Infrastructure. * Establish strict layer dependencies: Presentation -> Application -> Domain -> Infrastructure. * Use interfaces for communication between layers. * Employ Dependency Injection (DI) to decouple layers. **Don't Do This:** * Create cyclic dependencies between layers. * Bypass layers directly, e.g., Presentation accessing Infrastructure directly. * Implement business logic in the Presentation layer. **Why:** Layered architecture enhances code organization, testability, and maintainability. Clear separation of concerns reduces the impact of changes in one layer on other layers. **Code Example:** """csharp // Domain Layer public interface IOrderService { Order GetOrder(Guid orderId); void PlaceOrder(Order order); } public class Order { public Guid Id { get; set; } public DateTime OrderDate { get; set; } public decimal TotalAmount { get; set; } } // Infrastructure Layer public class OrderRepository : IOrderService { public Order GetOrder(Guid orderId) { // Retrieve order from database return new Order { Id = orderId, OrderDate = DateTime.Now, TotalAmount = 100 }; } public void PlaceOrder(Order order) { // Save order to database Console.WriteLine($"Order {order.Id} placed."); } } // Application Layer public class OrderAppService { private readonly IOrderService _orderService; public OrderAppService(IOrderService orderService) { _orderService = orderService; } public Order GetOrder(Guid orderId) { return _orderService.GetOrder(orderId); } public void PlaceOrder(Order order) { _orderService.PlaceOrder(order); } } // Presentation Layer (e.g., ASP.NET Core Controller) [ApiController] [Route("[controller]")] public class OrderController : ControllerBase { private readonly OrderAppService _orderAppService; public OrderController(OrderAppService orderAppService) { _orderAppService = orderAppService; } [HttpGet("{id}")] public ActionResult<Order> Get(Guid id) { var order = _orderAppService.GetOrder(id); if (order == null) { return NotFound(); } return order; } [HttpPost] public IActionResult Post([FromBody] Order order) { _orderAppService.PlaceOrder(order); return Ok(); } } """ **Anti-pattern:** A controller directly instantiating a data access class. ### 1.2. Domain-Driven Design (DDD) **Standard:** Apply DDD principles to model complex business domains effectively. **Do This:** * Define bounded contexts to encapsulate specific business functionalities. * Create a ubiquitous language understood by both developers and domain experts. * Model entities, value objects, aggregates, and domain services effectively. * Use repositories for data access. **Don't Do This:** * Create an anemic domain model with entities only containing properties. * Ignore the advice from domain experts. * Mix domain logic within infrastructure concerns. **Why:** DDD promotes a deeper understanding of the business domain, leading to more robust and maintainable software. **Code Example:** """csharp // Value Object public record Address(string Street, string City, string PostalCode, string Country); // Entity public class Customer { public Guid Id { get; private set; } public string Name { get; private set; } public Address Address { get; private set; } public Customer(Guid id, string name, Address address) { Id = id; Name = name; Address = address; } public void UpdateAddress(Address newAddress) { Address = newAddress; } } // Aggregate Root public class Order { public Guid Id { get; private set; } public Customer Customer { get; private set; } private readonly List<OrderItem> _orderItems = new(); public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly(); public DateTime OrderDate { get; private set; } public Order(Guid id, Customer customer, DateTime orderDate) { Id = id; Customer = customer; OrderDate = orderDate; } public void AddOrderItem(OrderItem item) { _orderItems.Add(item); } } // Domain Service public interface IOrderDiscountService { decimal CalculateDiscount(Order order); } public class OrderDiscountService : IOrderDiscountService { public decimal CalculateDiscount(Order order) { // Logic to calculate discount based on customer and order details return 0.1m; // 10% discount } } //Repository public interface IOrderRepository { Order? GetById(Guid id); void Add(Order order); void Update(Order order); void Delete(Guid id); } """ **Anti-pattern:** Exposing the internal collection "_orderItems" directly instead of using "AsReadOnly()". ### 1.3. Microservices Architecture **Standard:** If appropriate for the system's complexity, adopt a microservices architecture to enable independent deployment and scaling. **Do This:** * Design services around business capabilities. * Ensure services are independently deployable and scalable. * Implement robust inter-service communication mechanisms (e.g., REST, gRPC, message queues). * Use a decentralized data management approach. **Don't Do This:** * Create tightly coupled services. * Share databases between services. * Fall into the trap of a distributed monolith. **Why:** Microservices allow for greater flexibility, scalability, and resilience, particularly in large and complex systems. **Code Example:** """csharp // Customer Service (ASP.NET Core Web API) [ApiController] [Route("[controller]")] public class CustomerController : ControllerBase { [HttpGet("{id}")] public ActionResult<string> Get(Guid id) { // Retrieve customer information return $"Customer Name: John Doe, ID: {id}"; } } //Order Service (ASP.NET Core Web API) - Communication using HttpClient. public class OrderService{ private readonly HttpClient _httpClient; public OrderService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<string> GetCustomerName(Guid customerId) { var response = await _httpClient.GetAsync($"http://customer-service/customer/{customerId}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } // Use gRPC for high-performance communication // Define a gRPC service service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } // Implement the gRPC service in C# public class GreeterService : Greeter.GreeterBase { private readonly ILogger<GreeterService> _logger; public GreeterService(ILogger<GreeterService> logger) { _logger = logger; } public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } } """ **Anti-pattern:** Tightly coupling services by directly referencing each other's data models. ## 2. Project Structure and Organization ### 2.1. Solution Structure **Standard:** Organize the solution into logical projects based on layers (e.g., "MyProject.Presentation", "MyProject.Application", "MyProject.Domain", "MyProject.Infrastructure"). Include separate test projects for efficient unit and integrations tests (e.g. "MyProject.UnitTests"). **Do This:** * Create a separate project for each layer or bounded context. * Use a "src" folder to contain all source code projects. * Use a "tests" folder to contain all test projects. **Don't Do This:** * Place all code in a single project. * Mix different concerns within the same project. **Why:** Logical separation improves maintainability and reduces build times. **Example:** """ MySolution.sln ├── src │ ├── MyProject.Presentation │ │ └── MyProject.Presentation.csproj │ ├── MyProject.Application │ │ └── MyProject.Application.csproj │ ├── MyProject.Domain │ │ └── MyProject.Domain.csproj │ └── MyProject.Infrastructure │ └── MyProject.Infrastructure.csproj └── tests ├── MyProject.UnitTests │ └── MyProject.UnitTests.csproj └── MyProject.IntegrationTests └── MyProject.IntegrationTests.csproj """ ### 2.2. Namespace Conventions **Standard:** Use consistent and descriptive namespaces following the pattern "CompanyName.ProjectName.ModuleName". **Do This:** * Align namespaces with project structure. * Use meaningful names for namespaces. **Don't Do This:** * Use generic namespaces like "Helpers" or "Utilities" without a clear context. * Create namespace hierarchies that don't reflect the code structure. **Why:** Clear namespace conventions improve code discoverability and prevent naming conflicts. **Example:** """csharp namespace MyCompany.ECommerce.Orders { public class OrderService { // ... } } """ ### 2.3. Dependency Injection (DI) Container Configuration **Standard:** Use a DI container (e.g., Autofac, Microsoft.Extensions.DependencyInjection) to manage dependencies. **Do This:** * Configure DI in a dedicated class or method. * Register dependencies using interfaces. * Use constructor injection. **Don't Do This:** * Use the "new" keyword to create dependencies directly within classes. * Hardcode service locations or connection strings. **Why:** DI promotes loose coupling, testability, and maintainability. **Code Example:** """csharp // Program.cs (ASP.NET Core) public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Configure DI builder.Services.AddScoped<IOrderService, OrderRepository>(); builder.Services.AddScoped<OrderAppService>(); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } """ **Anti-pattern:** Using a service locator pattern instead of constructor injection. ### 2.4. Asynchronous Programming **Standard:** Utilize "async" and "await" for I/O-bound operations to avoid blocking threads. **Do This:** * Use "async" and "await" for database calls, network requests, and file I/O. * Follow the "Async" suffix convention for asynchronous methods (e.g., "GetOrderAsync"). * Configure "ConfigureAwait(false)" when the synchronization context is not needed. **Don't Do This:** * Use ".Result" or ".Wait()" which can cause deadlocks in ASP.NET applications. * Mix synchronous and asynchronous code without careful consideration. * Ignore exceptions thrown by asynchronous operations. **Why:** Asynchronous programming improves application responsiveness and scalability. **Code Example:** """csharp public interface IOrderService { Task<Order> GetOrderAsync(Guid orderId); } public class OrderRepository : IOrderService { public async Task<Order> GetOrderAsync(Guid orderId) { await Task.Delay(100); //Simulate work return new Order { Id = orderId, OrderDate = DateTime.Now, TotalAmount = 100 }; // Retrieve order from database asynchronously // await dbContext.Orders.FindAsync(orderId).ConfigureAwait(false); } } [ApiController] [Route("[controller]")] public class OrderController : ControllerBase { private readonly IOrderService _orderService; public OrderController(IOrderService orderService) { _orderService = orderService; } [HttpGet("{id}")] public async Task<ActionResult<Order>> Get(Guid id) { var order = await _orderService.GetOrderAsync(id); if (order == null) { return NotFound(); } return order; } } """ **Anti-pattern:** Performing CPU-bound operations using "async" and "await" without offloading to a separate thread. Use "Task.Run" for CPU-bound operations. ## 3. Common Anti-Patterns and Mistakes ### 3.1. God Classes **Issue:** Creating a single class that handles too many responsibilities. **Solution:** Break down the class into smaller, more focused classes following the Single Responsibility Principle (SRP). Apply DDD's bounded contexts to identify logical boundaries. ### 3.2. Feature Envy **Issue:** A method accessing the data of another object more than its own. **Solution:** Move the method to the class where the data resides or create a domain service to handle the operation. ### 3.3. Shotgun Surgery **Issue:** Making a single change requires modifications in multiple classes. **Solution:** Review the design and refactor to encapsulate the change within a single class or module. Look for opportunities to apply the Open/Closed Principle (OCP). ### 3.4. Premature Optimization **Issue:** Optimizing code before identifying actual performance bottlenecks. **Solution:** Measure performance using profiling tools and focus on optimizing the critical paths. "Make it work, make it right, make it fast." ### 3.5. Ignoring Security Concerns **Issue:** Neglecting security best practices, leading to vulnerabilities. **Solution:** Implement security measures such as input validation, output encoding, authentication, authorization, and protection against common attacks like SQL injection and cross-site scripting (XSS). Use static analysis tools to identify potential security vulnerabilities. ## 4. Modern Best Practices ### 4.1. Using Records **Standard:** Use Records for immutable data structures. Available since C# 9.0. **Do This:** * Use records for DTOs, value objects, and other immutable data carriers. * Take advantage of positional syntax for concise declaration. **Don't Do This:** * Use records for entities that require mutability. **Why:** Records provide a concise and safe way to represent immutable data. **Code Example:** """csharp public record Person(string FirstName, string LastName); // Can be instantiated as Person person = new("John", "Doe"); //With C# 10, you can declare record structs: public readonly record struct Point(double X, double Y); """ ### 4.2. Top-Level Statements **Standard:** Use top-level statements for console applications and simple programs. **Do This:** * Simplify the entry point of console applications. **Don't Do This:** * Use top-level statements in complex applications with multiple classes and dependencies. **Why:** Top-level statements reduce boilerplate code. **Code Example:** """csharp // Program.cs Console.WriteLine("Hello, World!"); """ ### 4.3. Target-Typed New Expressions **Standard:** Use target-typed new expressions to simplify object instantiation. **Do This:** * Use "new()" when the type is clear from the context. **Don't Do This:** * Use "new()" when the type is ambiguous. **Why:** Target-typed new expressions reduce redundancy and improve readability. **Code Example:** """csharp List<string> names = new(); // Instead of new List<string>() """ ### 4.4. Global Usings **Standard:** Utilize global usings to reduce redundancy in using directives. Available since C# 10.0. **Do This:** * Add common namespaces to a global using directives file to avoid repeating "using" statements in every file. **Don't Do This:** * Globally import namespaces that are rarely used as this can pollute the scope. **Why:** Global usings improve code clarity and reduce visual clutter. **Code Example:** Create a file (e.g., "GlobalUsings.cs") in your project with the following content: """csharp global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading.Tasks; """ Now, these namespaces are available in all C# files within the project without needing to explicitly add "using" directives. ### 4.5 Pattern Matching Enhancements **Standard:** Leverage improved pattern matching for concise and expressive code. **Do This:** * Utilize property patterns, positional patterns, and "switch" expressions for complex conditional logic. **Don't Do This:** * Overuse pattern matching for simple conditions, as it can reduce readability. Replace "if-else". **Why:** Pattern matching makes code more readable and maintainable by expressing complex conditions in a declarative way. **Code Example:** """csharp public record Address(string Street, string City, string ZipCode, string Country); public record Person(string FirstName, string LastName, Address Address); public static string GetAddressInfo(Person person) { return person switch { { Address : { Country: "USA", ZipCode: "90210" } } => "Beverly Hills Resident", { Address.Country: "USA" } => "USA Resident", { Address.Country: "Canada" } => "Canadian Resident", _ => "Other Resident" }; } """ ## 5. Technology-Specific Details ### 5.1. ASP.NET Core **Standard:** Follow ASP.NET Core best practices for building web applications. * Use middleware for request processing. * Implement dependency injection (DI) for managing dependencies. * Use logging abstractions for consistent logging. * Apply security best practices (authentication, authorization, data protection). * Leverage minimal APIs based on project need. ### 5.2. Entity Framework Core **Standard:** Adhere to EF Core best practices for data access. * Use asynchronous operations for database interactions. * Avoid loading unnecessary data using projections and filtering. * Implement connection resiliency and retry logic. * Employ code-first migrations for database schema management. * Utilize the repository pattern to abstract data access logic. ### 5.3. C# 12 features C# 12 introduces primary constructors, collection expressions, inline arrays, default lambda expressions, and alias any type. Make sure that the development team understands these new features and uses them accordingly. ## 6. Conclusion These core architecture standards provide a foundation for building robust, maintainable, and scalable C# applications. By following these guidelines, development teams can ensure consistency, improve code quality, and reduce technical debt. Remember to adapt these standards to the specific needs of your project and organization, and regularly review and update them as new technologies and best practices emerge.
# State Management Standards for C# This document outlines the recommended standards and best practices for managing state in C# applications. Effective state management is crucial for building maintainable, scalable, and performant applications. These guidelines cover various approaches, patterns, and techniques, focusing on modern C# features and the .NET ecosystem. ## 1. Introduction to State Management in C# State management involves handling the data an application uses and how it changes over time. In C#, this can range from simple variable assignments within a method to complex architectures for managing application-wide data across multiple layers and components. Incorrect state management can lead to bugs that are hard to track, performance bottlenecks, and difficulties in maintaining the application over time. ### 1.1 Key Principles of State Management * **Single Source of Truth:** Ensure that each piece of data has one authoritative source to prevent inconsistencies. * **Immutability:** Favor immutable data structures to simplify reasoning about state changes and prevent unintended side effects. When state needs to change, create a new instance reflecting the changes. * **Explicit State Transitions:** Make state transitions explicit and predictable to improve debugging and maintainability. * **Separation of Concerns:** Decouple state management logic from UI and business logic to improve testability and reusability. * **Reactivity:** Utilize Reactive programming to propagate state changes to the UI and application using observable streams. ## 2. Local State Management Local state refers to data that is confined to a specific scope, such as a method or a class instance. ### 2.1 Variables and Fields #### Standard 1: Declare variables as close as possible to their usage. * **Do This:** """csharp public void ProcessData(List<string> items) { // ... other code ... if (items != null && items.Any()) { int processedCount = 0; // Declare variable close to usage foreach (var item in items) { // ... process item ... processedCount++; } Console.WriteLine($"Processed {processedCount} items."); } } """ * **Don't Do This:** """csharp public void ProcessData(List<string> items) { int processedCount; // Declared too early // ... other code ... if (items != null && items.Any()) { processedCount = 0; foreach (var item in items) { // ... process item ... processedCount++; } Console.WriteLine($"Processed {processedCount} items."); } } """ * **Why:** Reduces cognitive load, improves readability, and minimizes the risk of using uninitialized variables. #### Standard 2: Use "readonly" for fields that should not change after initialization. * **Do This:** """csharp public class Configuration { public readonly string ApiEndpoint; public Configuration(string apiEndpoint) { ApiEndpoint = apiEndpoint; } } """ * **Don't Do This:** """csharp public class Configuration { public string ApiEndpoint { get; set; } // Mutable! public Configuration(string apiEndpoint) { ApiEndpoint = apiEndpoint; } } """ * **Why:** Enforces immutability, making the code safer and easier to reason about. #### Standard 3: Prefer immutable data structures locally. * **Do This:** C# 9 introduced "init" only setters which are ideal for initializing immutable objects. """csharp public record Person(string FirstName, string LastName) { public string FullName { get; init; } = $"{FirstName} {LastName}"; } public void Example() { var person = new Person("John", "Doe") { FullName = "Johnny Doe"}; //Use "init" to initialize } """ * **Don't Do This:** """csharp public class Person { public string FirstName { get; set; } public string LastName { get; set; } } """ * **Why:** Simplifies debugging by preventing accidental modifications to shared state within a specific scope. ### 2.2 Caching #### Standard 4: Use "MemoryCache" for simple, in-memory caching needs. * **Do This:** """csharp using Microsoft.Extensions.Caching.Memory; public class DataService { private readonly IMemoryCache _cache; public DataService(IMemoryCache cache) { _cache = cache; } public async Task<string> GetDataAsync(string key) { return await _cache.GetOrCreateAsync(key, async entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(10); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return await FetchDataFromSourceAsync(key); }); } private async Task<string> FetchDataFromSourceAsync(string key) { // Simulate fetching data await Task.Delay(100); return $"Data for {key} fetched from source"; } } """ * **Why:** Provides a simple and efficient way to cache data in memory, reducing latency and improving performance. #### Standard 5: Properly invalidate cache entries when the underlying data changes. * **Do This:** """csharp public async Task UpdateDataAsync(string key, string newData) { // Update the underlying data source. // ... // Invalidate the cache entry. _cache.Remove(key); } """ * **Why:** Avoids serving stale data to users, ensuring data consistency. ## 3. Application State Management Application state refers to data that needs to be shared across multiple components of an application, often spanning different layers or modules. ### 3.1 Dependency Injection (DI) #### Standard 6: Use DI to manage the lifecycle of stateful objects. * **Do This:** """csharp using Microsoft.Extensions.DependencyInjection; public interface IDataService { Task<string> GetDataAsync(string key); } public class DataService : IDataService { private readonly IMemoryCache _cache; public DataService(IMemoryCache cache) { _cache = cache; } public async Task<string> GetDataAsync(string key) { // Retrieve or create data with MemoryCache return await _cache.GetOrCreateAsync(key, async entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(10); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return await FetchDataFromSourceAsync(key); }); } private async Task<string> FetchDataFromSourceAsync(string key) { // Simulate fetching data await Task.Delay(100); return $"Data for {key} fetched from source"; } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMemoryCache(); // Configures MemoryCache implementation services.AddScoped<IDataService, DataService>(); } } """ * **Why:** Enables loose coupling, improves testability, and simplifies the management of shared state. #### Standard 7: Choose appropriate DI scopes based on the state's lifecycle requirements (Singleton, Scoped, Transient). * **Singleton:** Use for application-wide settings, configuration, and read-only data. * **Scoped:** Use for data that is specific to a user session or request. * **Transient:** Use for objects that should be recreated every time they are requested. ### 3.2 Options Pattern #### Standard 8: Use the Options pattern to configure components with external settings. * **Do This:** """csharp using Microsoft.Extensions.Options; public class AppSettings { public string ApiUrl { get; set; } public int Timeout { get; set; } } public class MyService { private readonly AppSettings _settings; public MyService(IOptions<AppSettings> settings) { _settings = settings.Value; } public void DoSomething() { Console.WriteLine($"API URL: {_settings.ApiUrl}, Timeout: {_settings.Timeout}"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<AppSettings>(configuration.GetSection("AppSettings")); services.AddTransient<MyService>(); } } """ * **Why:** Provides a type-safe way to access configuration settings, simplifies testing and reduces the risk of errors. Supports hot reloading of configuration in .NET 6+. ### 3.3 Centralized State Management with Redux/Flux-like Patterns For complex applications with intricate global state, consider patterns like Redux or Flux. While not a direct implementation, libraries like Fluxor provide a C# adaptation of these patterns. These patterns center around a unidirectional data flow and a central store to manage application state. #### Standard 9: Implement a Redux/Flux-like pattern for complex state management. * **Do This (using Fluxor):** """csharp // State public class TodoState { public ImmutableArray<string> Todos { get; init; } public bool IsLoading { get; init; } } // Feature public class TodoFeature : Feature<TodoState> { public override string GetName() => "Todo"; protected override TodoState GetInitialState() => new TodoState { Todos = ImmutableArray<string>.Empty, IsLoading = false }; } // Actions public record AddTodoAction(string Todo); public record LoadTodosAction(); public record LoadTodosResultAction(ImmutableArray<string> Todos); // Reducers public static class TodoReducers { [ReducerMethod] public static TodoState ReduceAddTodoAction(TodoState state, AddTodoAction action) => state with { Todos = state.Todos.Add(action.Todo) }; // Use "with" to create a new instance [ReducerMethod] public static TodoState ReduceLoadTodosAction(TodoState state, LoadTodosAction action) => state with { IsLoading = true }; [ReducerMethod] public static TodoState ReduceLoadTodosResultAction(TodoState state, LoadTodosResultAction action) => state with { Todos = action.Todos, IsLoading = false }; } // Effects - Side effects public class TodoEffects { private readonly HttpClient _http; public TodoEffects(HttpClient http) { _http = http; } [EffectMethod] public async Task HandleLoadTodosAction(LoadTodosAction action, IDispatcher dispatcher) { var todos = await _http.GetFromJsonAsync<string[]>("/api/todos"); dispatcher.Dispatch(new LoadTodosResultAction(todos.ToImmutableArray())); } } // Component example public class TodoComponent : ComponentBase { [Inject] private IState<TodoState> TodoState { get; set; } [Inject] private IDispatcher Dispatcher { get; set; } protected override void OnInitialized() { base.OnInitialized(); Dispatcher.Dispatch(new LoadTodosAction()); } private void AddTodo(string todo) { Dispatcher.Dispatch(new AddTodoAction(todo)); } // UI logic to display todos } // In your Program.cs or main bootstrapping: public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .ConfigureServices((hostContext, services) => { services.AddFluxor(options => { options.ScanAssemblies(typeof(Program).Assembly); options.UseReduxDevTools(); // Optional Redux DevTools integration. Requires browser extension. }); services.AddScoped<HttpClient>(); }); """ * **Why:** * Provides a predictable and centralized approach to managing application state. * Simplifies debugging and testing by enforcing a unidirectional data flow. * Enables time-travel debugging with tools like Redux DevTools. * "with" keyword (records) promotes immutable state updates. * Effects handle side effects (async calls, etc.) separate from reducers which makes code easier to maintain and more testable. #### Standard 10: Avoid directly modifying the state store. Only modify state through actions and reducers. * **Why:** Ensures that all state changes are predictable and traceable, improving debugging and maintainability. Violating this principle negates one of the main benefits of the pattern. ### 3.4 Reactive Programming with Rx.NET Rx.NET (Reactive Extensions for .NET) enables handling asynchronous data streams and change propagation in a declarative and composable manner. #### Standard 11: Use "IObservable<T>" for propagating state changes asynchronously. * **Do This:** """csharp using System; using System.Reactive.Subjects; public class DataService { private readonly Subject<string> _dataStream = new Subject<string>(); public IObservable<string> DataStream => _dataStream; public void UpdateData(string newData) { _dataStream.OnNext(newData); } } public class UIComponent { private readonly DataService _dataService; private IDisposable _subscription; public UIComponent(DataService dataService) { _dataService = dataService; } public void SubscribeToDataChanges() { _subscription = _dataService.DataStream.Subscribe(data => { // Update UI with new data Console.WriteLine($"UI received: {data}"); }); } public void Unsubscribe() { _subscription?.Dispose(); } } """ * **Why:** * Enables reactive updates in the UI or other parts of the application. * Provides a clean and efficient way to handle asynchronous data streams. * Allows you to compose complex data transformations and filtering using LINQ-like operators. #### Standard 12: Dispose of "IDisposable" subscriptions to prevent memory leaks. * **Why:** Essential to avoid holding references to objects that are no longer needed. Common Anti-Pattern: Ignoring Resources: Not disposing of IDisposable resources leads to resource leaks and eventual application instability. Use "using" statements or try-finally blocks. ## 4. Data Persistence How to persist any state outside of an app's lifecycle ### 4.1 Entity Framework Core (EF Core) #### Standard 13: Use EF Core for database access, employing best practices for performance and security. * **Do This:** """csharp using Microsoft.EntityFrameworkCore; public class BloggingContext : DbContext { public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { } public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } public class BlogService { private readonly BloggingContext _context; public BlogService(BloggingContext context) { _context = context; } public async Task<List<Blog>> GetBlogsAsync() { return await _context.Blogs.ToListAsync(); } public async Task AddBlogAsync(Blog blog) { _context.Blogs.Add(blog); await _context.SaveChangesAsync(); } } """ * **Why:** Provides an ORM for interacting with databases, simplifying data access and management. #### Standard 14: Use asynchronous operations ("async"/"await") for all database interactions to avoid blocking the UI or other threads. * **Don't Do This:** """csharp public List<Blog> GetBlogs() //Synchronous, can block the thread { return _context.Blogs.ToList(); } """ #### Standard 15: Protect against SQL injection by using parameterized queries or EF Core's built-in mechanisms. * **Do This:** """csharp var searchTerm = "example"; var blogs = _context.Blogs.Where(b => EF.Functions.Like(b.Url, $"%{searchTerm}%")).ToList(); """ * **Don't Do This:** """csharp var searchTerm = "example"; var blogs = _context.Blogs.FromSqlRaw($"SELECT * FROM Blogs WHERE Url LIKE '%{searchTerm}%'").ToList(); //Vulnerable! User input not sanitized. """ #### Standard 16: Configure appropriate database contexts scopes (Scoped is best practice for web apps). ### 4.2 Using Distributed Caching (Redis) For applications that need to share state across multiple servers, distributed caching is an important technique. #### Standard 17: Use Redis (or another distributed cache) for session state. * **Do This:** """csharp using Microsoft.Extensions.Caching.Distributed; using System.Text.Json; public class SessionService { private readonly IDistributedCache _cache; public SessionService(IDistributedCache cache) { _cache = cache; } public async Task SetSessionDataAsync<T>(string key, T data) { var options = new DistributedCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromMinutes(20)); var jsonData = JsonSerializer.Serialize(data); await _cache.SetStringAsync(key, jsonData, options); } public async Task<T> GetSessionDataAsync<T>(string key) { var jsonData = await _cache.GetStringAsync(key); if (jsonData == null) { return default; } return JsonSerializer.Deserialize<T>(jsonData); } public async Task RemoveSessionDataAsync(string key) { await _cache.RemoveAsync(key); } } // Configure Redis in Startup.cs public void ConfigureServices(IServiceCollection services) { services.AddStackExchangeRedisCache(options => { options.Configuration = Configuration.GetConnectionString("Redis"); // Connection string options.InstanceName = "MyAppName:"; // Optional prefix }); services.AddScoped<SessionService>(); } """ * **Why:** Allows you to share data across multiple instances of your application, which is necessary for scaling out. ## 5. Conclusion Following these standards will enable you to write clearer, more performant, and more maintainable C# code, particularly when dealing with application state management. These are general guidelines, and the specifics should be refined based on the particular needs of each project. Furthermore, remember that constantly evolving C# language and technologies necessitate continuous learning and adjustment of these coding standards.
# Testing Methodologies Standards for C# This document outlines the testing methodologies standards to be followed when developing C# applications. It covers unit, integration, and end-to-end testing strategies, along with best practices, anti-patterns, and practical C# examples. ## 1. Introduction to Testing in C# Effective testing is crucial for ensuring the reliability, maintainability, and correctness of C# applications. A well-structured testing strategy helps identify defects early in the development lifecycle, reducing overall costs and improving software quality. ### 1.1. Types of Tests * **Unit Tests:** Test individual components or methods in isolation. * **Integration Tests:** Test the interactions between multiple components or modules. * **End-to-End (E2E) Tests:** Test the entire application flow from start to finish, simulating real user scenarios. ### 1.2. Why Testing Matters * **Defect Prevention:** Early detection of issues reduces costs. * **Maintainability:** Tests act as living documentation, facilitating refactoring and code changes. * **Reliability:** Thorough testing ensures the application functions as expected under various conditions. * **Confidence:** Developers can confidently deploy and maintain applications knowing they are well-tested. ## 2. Unit Testing Standards Unit testing involves testing individual components in isolation. This requires mocking dependencies and ensuring that each unit of code behaves as expected. ### 2.1. Principles of Unit Testing * **Focus on Individual Units:** Test one method or class at a time. * **Isolation:** Use mocks and stubs to isolate the unit under test. * **Fast Execution:** Unit tests should run quickly to provide immediate feedback. * **Repeatable:** Results should be consistent every time the tests are run. * **Automated:** Tests should be automated as part of the build process. ### 2.2. Do This * **Use a Unit Testing Framework:** Utilize frameworks like xUnit, NUnit, or MSTest. * **Follow the AAA Pattern:** Arrange, Act, Assert. * **Write Clear and Concise Tests:** Each test should have a specific purpose. * **Use Meaningful Test Names:** Test names should describe what is being tested. * **Test All Scenarios:** Include positive, negative, and boundary conditions. * **Use Mocking Frameworks:** Use libraries like Moq or NSubstitute for mocking dependencies. * **Strive for High Code Coverage:** Aim for 80-100% coverage in critical modules. ### 2.3. Don't Do This * **Test Multiple Units in a Single Test:** Each test should focus on one unit. * **Depend on External Resources:** Avoid using databases or network resources in unit tests directly. Use mocks instead. * **Write Tests That are Difficult to Understand:** Tests should be clear and self-explanatory. * **Ignore Test Failures:** Investigate and fix failing tests immediately. * **Mock Everything:** Only mock dependencies that are necessary for isolation. ### 2.4. Code Examples #### Using xUnit """csharp using Xunit; using Moq; using System; public interface ICalculator { int Add(int a, int b); } public class CalculatorService { private readonly ICalculator _calculator; public CalculatorService(ICalculator calculator) { _calculator = calculator; } public int PerformCalculation(int a, int b) { return _calculator.Add(a, b); } } public class CalculatorServiceTests { [Fact] public void PerformCalculation_ValidInput_ReturnsSum() { // Arrange var mockCalculator = new Mock<ICalculator>(); mockCalculator.Setup(c => c.Add(2, 3)).Returns(5); var calculatorService = new CalculatorService(mockCalculator.Object); // Act int result = calculatorService.PerformCalculation(2, 3); // Assert Assert.Equal(5, result); mockCalculator.Verify(c => c.Add(2, 3), Times.Once); } } """ **Explanation:** * This example uses "xUnit" for the testing framework and "Moq" for mocking. * "ICalculator" is an interface that "CalculatorService" depends on. * The "PerformCalculation_ValidInput_ReturnsSum" test mocks the "ICalculator" and verifies that "Add" is called with the correct parameters and that the result is as expected. * The "AAA" pattern (Arrange, Act, Assert) is explicitly used for clarity. #### Using NUnit """csharp using NUnit.Framework; using Moq; public interface IOperation { int Execute(int x, int y); } public class OperationService { private readonly IOperation _operation; public OperationService(IOperation operation) { _operation = operation; } public int Calculate(int a, int b) { return _operation.Execute(a, b); } } [TestFixture] public class OperationServiceTests { [Test] public void Calculate_ValidInput_ReturnsResult() { // Arrange var mockOperation = new Mock<IOperation>(); mockOperation.Setup(op => op.Execute(4, 5)).Returns(9); var operationService = new OperationService(mockOperation.Object); // Act int result = operationService.Calculate(4, 5); // Assert Assert.AreEqual(9, result); mockOperation.Verify(op => op.Execute(4, 5), Times.Once); } } """ **Explanation:** * This example uses "NUnit" as the testing framework and "Moq" for mocking. * The "OperationService" class depends on the "IOperation" interface. * In the unit test, a mock of "IOperation" is created. The "Setup" method is used to define the behavior of the mock when "Execute" is called with specific arguments. * The Act section calls the "Calculate" method on the "OperationService" instance. * The Assert section verifies the result and ensures the mocked method was called as expected. ### 2.5. Common Anti-Patterns * **God Class Tests:** Testing multiple functionalities in a single test method. * **Chatty Tests:** Over-mocking or verifying too many interactions. * **Fragile Tests:** Tests that break easily with minor code changes. * **Ignoring Edge Cases:** Failing to test boundary conditions or edge cases. ## 3. Integration Testing Standards Integration testing involves testing the interaction between different components or modules of the application. ### 3.1. Principles of Integration Testing * **Focus on Interactions:** Verify that components work together correctly. * **Use Real Dependencies:** Integrate with real databases or services when appropriate. * **Test Data Isolation:** Ensure that tests do not interfere with each other's data. * **Automated:** Integration tests should be automated for continuous integration. ### 3.2. Do This * **Define Clear Integration Points:** Identify key areas where components interact. * **Use Test Databases:** Set up dedicated test databases to avoid affecting production data. * **Seed Data:** Populate the database with test data before running tests. * **Cleanup Data:** Clean up the test database after running tests. * **Use Dependency Injection:** Facilitate integration by using dependency injection to configure components. ### 3.3. Don't Do This * **Test Everything at Once:** Break down integration tests into smaller, manageable pieces. * **Use Production Data:** Never use production data in integration tests. * **Ignore Error Handling:** Test how components handle errors when interacting with each other. ### 3.4. Code Examples #### Using WebApplicationFactory for Integration Testing """csharp using Microsoft.AspNetCore.Mvc.Testing; using System.Net; using System.Threading.Tasks; using Xunit; public class WebAppFactoryTests : IClassFixture<WebApplicationFactory<Program>> { private readonly WebApplicationFactory<Program> _factory; public WebAppFactoryTests(WebApplicationFactory<Program> factory) { _factory = factory; } [Fact] public async Task Get_EndpointsReturnSuccessAndCorrectContentType() { // Arrange var client = _factory.CreateClient(); // Act var response = await client.GetAsync("/WeatherForecast"); // Assert response.EnsureSuccessStatusCode(); // Status Code 200-299 Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType.ToString()); } [Fact] public async Task Post_ValidInput_ReturnsCreated() { // Arrange var client = _factory.CreateClient(); var content = new StringContent("{ \"Data\": \"Test Data\" }", System.Text.Encoding.UTF8, "application/json"); // Act var response = await client.PostAsync("/api/Data", content); // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); } } """ **Explanation:** * This example uses "WebApplicationFactory" to create an in-memory instance of an ASP.NET Core application for integration testing. * The "Get_EndpointsReturnSuccessAndCorrectContentType" test verifies that the "/WeatherForecast" endpoint returns a successful status code and the correct content type. * The "Post_ValidInput_ReturnsCreated" test sends a "POST" request to the "/api/Data" endpoint and verifies that it returns a "Created" status code. ### 3.5. Common Anti-Patterns * **Brittle Integration Tests:** Tests that rely too heavily on implementation details. * **Lack of Data Isolation:** Tests that interfere with each other's data. * **Ignoring Error Scenarios:** Failing to test how components handle errors when integrating with each other. ## 4. End-to-End (E2E) Testing Standards End-to-end testing verifies that the entire application works as expected from start to finish, simulating real user scenarios. ### 4.1. Principles of E2E Testing * **Simulate Real User Scenarios:** Test the most common user workflows. * **Use Real Environments:** Test in environments that closely resemble production. * **Automated:** E2E tests should be automated and run frequently. * **Robust:** Tests should be resilient to minor UI changes. ### 4.2. Do This * **Prioritize Key Scenarios:** Focus on testing the most critical user workflows. * **Use a Testing Framework:** Utilize frameworks like Selenium, Playwright, or Cypress. * **Write Clear Test Cases:** Test cases should describe the steps a user would take. * **Use Page Object Model (POM):** Design tests using the page object model for maintainability. * **Run Tests in Parallel:** Speed up test execution by running tests in parallel. ### 4.3. Don't Do This * **Test Every Possible Scenario:** Focus on the most important ones. * **Make Tests Too Specific:** Keep tests flexible enough to handle minor UI changes. * **Ignore Test Failures:** Investigate and fix failing tests promptly. ### 4.4. Code Examples #### Using Playwright for E2E Testing """csharp using Microsoft.Playwright; using Microsoft.Playwright.NUnit; using NUnit.Framework; using System.Threading.Tasks; public class ExampleTests : PageTest { [Test] public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingToTheIntroPage() { await Page.GotoAsync("https://playwright.dev/"); // Expect a title "to contain" a substring. await Expect(Page).ToHaveTitleAsync(new System.Text.RegularExpressions.Regex("Playwright")); // create a locator var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }); // Expect an attribute "to be strictly equal" to the expected value. await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro"); // Click the get started link. await getStarted.ClickAsync(); // Expects the URL to contain intro. await Expect(Page).ToHaveURLAsync(new System.Text.RegularExpressions.Regex(".*intro")); } } """ **Explanation:** * This example uses Playwright for end-to-end testing. * The "HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingToTheIntroPage" test navigates to the Playwright website, verifies the title, finds a link, confirms an attribute, clicks the link, and finally verifies the URL. #### Using Selenium with C# """csharp using NUnit.Framework; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using System; [TestFixture] public class SeleniumTests { private IWebDriver driver; [SetUp] public void Setup() { // Initialize ChromeDriver driver = new ChromeDriver(); driver.Manage().Window.Maximize(); } [Test] public void SearchGoogle() { // Navigate to Google driver.Navigate().GoToUrl("https://www.google.com"); // Find the search box, enter text, and submit IWebElement searchBox = driver.FindElement(By.Name("q")); searchBox.SendKeys("Selenium C#"); searchBox.Submit(); // Wait for the search results page to load System.Threading.Thread.Sleep(2000); // Assert that the title contains the search query Assert.IsTrue(driver.Title.Contains("Selenium C#")); } [TearDown] public void TearDown() { // Close the browser driver.Quit(); } } """ **Explanation:** * This example uses Selenium to automate browser interactions. * The "SearchGoogle" test navigates to Google, enters a search query, and asserts that the title contains the search query. * The "Setup" and "TearDown" methods initialize and close the browser, respectively. ### 4.5. Common Anti-Patterns * **Flaky Tests:** Tests that pass or fail intermittently. * **Slow Tests:** Tests that take too long to execute, slowing down the development process. * **Lack of Isolation:** Tests that depend on external factors, such as network connectivity. ## 5. Performance Testing Performance testing is critical to ensure that C# applications perform efficiently under expected loads. It includes load testing, stress testing, and soak testing. ### 5.1. Principles of Performance Testing * **Real-world Scenarios:** Simulate realistic user behavior. * **Measure Key Metrics:** Monitor response times, throughput, and resource utilization. * **Identify Bottlenecks:** Find and address performance issues early in the development cycle. * **Automate Tests:** Incorporate performance tests into the CI/CD pipeline. ### 5.2. Do This * **Define Performance Goals:** Set clear performance targets based on business requirements. * **Use Performance Testing Tools:** Employ tools like JMeter, LoadView, or K6 for load testing. * **Monitor Server Resources:** Track CPU utilization, memory usage, and disk I/O during tests. ### 5.3. Don't Do This * **Ignore Performance Issues:** Address performance issues as seriously as functional bugs. * **Test in Isolation:** Test performance in an environment that closely resembles production. ### 5.4. Code Examples #### Using K6 for Performance Testing (example from K6 documentation) While K6 is typically run from the command line or a script, the configuration can be specified in C# via executing a process . You can then analyze the results in your C# code or integrate them into your testing framework. """csharp using System.Diagnostics; using System.Threading.Tasks; using Xunit; public class K6PerformanceTests { [Fact] public async Task BasicLoadTest() { // Arrange string k6ScriptPath = "path/to/your/k6script.js"; // Replace with actual path. The actual K6 script is still JS string k6Command = $"run {k6ScriptPath}"; // Act var processInfo = new ProcessStartInfo { FileName = "k6", Arguments = k6Command, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using (var process = new Process()) { process.StartInfo = processInfo; process.Start(); string output = await process.StandardOutput.ReadToEndAsync(); string error = await process.StandardError.ReadToEndAsync(); process.WaitForExit(); Xunit.Assert.Equal(0, process.ExitCode); // Assert that K6 ran successfully Xunit.Assert.True(string.IsNullOrEmpty(error), $"K6 reported errors: {error}"); // TODO: Add assertions based on the K6 output. Parse the output/JSON summary to enforce SLOs System.Console.WriteLine(output); //For debug/inspection of the K6 Results } } } """ **k6script.js (Example K6 script)** """javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { vus: 10, //Virtual Users duration: '10s', }; export default function () { let res = http.get('https://test.k6.io'); check(res, { 'status is 200': (r) => r.status === 200, }); sleep(1); } """ **Explanation:** * This example shows running a K6 performance test from a C# test. * The C# code creates a process to execute the K6 command-line tool. * The K6 script (k6script.js) defines the load test scenario. * This approach lets you integrate performance testing into your existing C# testing framework. You would parse the output to check assertions and Service Level Objectives/SLOs ### 5.5. Common Anti-Patterns * **Ignoring Performance Testing:** Neglecting to test performance until late in the development cycle. * **Inadequate Load:** Not simulating realistic user loads during testing. * **Lack of Monitoring:** Failing to monitor key performance metrics. ## 6. Security Testing Standards Security testing is essential to ensure that C# applications are protected against vulnerabilities. ### 6.1. Principles of Security Testing * **Identify Vulnerabilities:** Detect potential security flaws early. * **Simulate Attacks:** Mimic real-world attack scenarios to uncover weaknesses. * **Automate Tests:** Include security tests as part of the CI/CD pipeline. * **Regularly Update Tests:** Keep security tests up-to-date with the latest threats. ### 6.2. Do This * **Perform Static Analysis:** Use tools like SonarQube or Fortify to identify code vulnerabilities. * **Conduct Dynamic Analysis:** Employ tools like OWASP ZAP or Burp Suite to test running applications for security flaws. * **Implement Penetration Testing:** Hire ethical hackers to perform manual penetration testing. * **Follow Security Best Practices:** Adhere to secure coding principles and guidelines. * **Use SAST/DAST Tools**: Integrate Static Application Security Testing (SAST) and Dynamic Application Security Testing (DAST) into your CI/CD pipeline. ### 6.3. Don't Do This * **Ignore Security Testing:** Neglecting security testing increases the risk of vulnerabilities. * **Rely Solely on Automated Tools:** Manual testing is also necessary to identify complex vulnerabilities. * **Store Sensitive Data in Code:** Avoid storing passwords or API keys directly in the codebase. ### 6.4. Code Examples #### Example using Static Analysis with Roslyn Analyzers """csharp // Install the Microsoft.CodeAnalysis.FxCopAnalyzers NuGet package // Enable rule CA5398 "Ensure the anti-forgery token is updated when calling methods that require it." using Microsoft.AspNetCore.Mvc; public class HomeController : Controller { [HttpPost] [ValidateAntiForgeryToken] //Ensures protection against Cross-Site Request Forgery (CSRF) attacks. public IActionResult UpdateData(string data) { // Update data logic return View(); } [HttpGet] //Without ValidateAntiForgeryToken, there is still a SAST warning (CA5398) public IActionResult GetData() { // Get data logic return View(); } } """ **Explanation:** * This example shows the use of Roslyn analyzers to perform static analysis. * The "ValidateAntiForgeryToken" attribute helps protect against Cross-Site Request Forgery (CSRF) attacks. The Roslyn analyzer would prompt you if you missed it on a POST. * Static analysis tools can automatically identify potential vulnerabilities in the code. ### 6.5. Common Anti-Patterns * **Hardcoding Credentials:** Storing sensitive information directly in the code. * **Using Weak Encryption:** Employing outdated or insecure encryption algorithms. * **Ignoring Input Validation:** Failing to validate user input, leading to injection attacks. * **Insufficient Access Control:** Allowing unauthorized access to sensitive data or functionality. ## 7. Test-Driven Development (TDD) TDD is a development approach where tests are written before the code. This ensures code is testable and meets the desired requirements. ### 7.1. TDD Cycle * **Red:** Write a failing test. * **Green:** Write the minimum code to pass the test. * **Refactor:** Improve the code while ensuring the test still passes. ### 7.2. Benefits of TDD * **Improved Code Quality:** Code is inherently testable. * **Reduced Defects:** Bugs are identified early in the development process. * **Living Documentation:** Tests serve as documentation of how the code should behave. ### 7.3. Do This * **Write Tests First:** Always write a failing test before writing any code. * **Keep Tests Small:** Each test should focus on a single aspect of the code. * **Refactor Regularly:** Improve the code and tests as needed. ### 7.4. Don't Do This * **Write Code Without Tests:** Avoid writing code without a corresponding test. * **Ignore Failing Tests:** Investigate and fix failing tests promptly. ## 8. Conclusion Adhering to these testing methodologies standards will significantly improve the quality, reliability, and maintainability of C# applications. By incorporating unit, integration, and end-to-end testing, along with performance and security testing, developers can ensure that their applications meet the highest standards of quality and security. Continuously updating and refining these standards based on the latest C# features and best practices is crucial for maintaining a robust and efficient development process.
# API Integration Standards for C# This document outlines the coding standards for integrating with APIs in C# applications. It covers patterns, best practices, and anti-patterns to ensure maintainable, performant, and secure API integrations. The standards are tailored for modern C# development, leveraging the latest language features and libraries. ## 1. Architectural Considerations for API Integration Before diving into code-level specifics, consider the overall architecture of your application and how API integrations fit within it. ### 1.1. Standard: Favor a Centralized API Client Layer **Do This:** Implement a dedicated layer or class library responsible for all API interactions. This layer encapsulates the complexities of API calls, authentication, and data transformation. **Don't Do This:** Scatter API calls directly within application logic (e.g., UI code, business logic). **Why:** * **Maintainability:** Changes to API endpoints, authentication mechanisms, or data formats only need to be updated in one place. * **Testability:** Isolates the API integration logic, making it easier to mock and test. * **Reusability:** API client classes can be reused across different parts of the application. **Code Example:** """csharp // API Client Interface public interface IApiClient { Task<ApiResponse<T>> GetAsync<T>(string endpoint, CancellationToken cancellationToken); Task<ApiResponse<T>> PostAsync<T>(string endpoint, object data, CancellationToken cancellationToken); // Other HTTP methods as needed } // Concrete Implementation using HttpClient public class ApiClient : IApiClient { private readonly HttpClient _httpClient; public ApiClient(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public async Task<ApiResponse<T>> GetAsync<T>(string endpoint, CancellationToken cancellationToken) { try { var response = await _httpClient.GetAsync(endpoint, cancellationToken); response.EnsureSuccessStatusCode(); // Throw exception for non-success status codes var content = await response.Content.ReadAsStringAsync(cancellationToken); var result = JsonSerializer.Deserialize<T>(content); return new ApiResponse<T> { Data = result, IsSuccess = true }; } catch (HttpRequestException ex) { // Handle HTTP request exceptions (e.g., network errors, timeouts) Console.Error.WriteLine($"API Request Failed: {ex.Message}"); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = ex.Message }; } catch (JsonException ex) { // Handle JSON serialization/deserialization exceptions Console.Error.WriteLine($"JSON Deserialization Failed: {ex.Message}"); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = "Invalid JSON response." }; } catch (Exception ex) { // Catch other potential exceptions (logging, custom error handling) Console.Error.WriteLine($"An unexpected error occurred: {ex.Message}"); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = "An unexpected error occurred." }; } } public async Task<ApiResponse<T>> PostAsync<T>(string endpoint, object data, CancellationToken cancellationToken) { // Implementation for POST requests (similar structure to GetAsync) try { var json = JsonSerializer.Serialize(data); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(endpoint, content, cancellationToken); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); var result = JsonSerializer.Deserialize<T>(responseContent); return new ApiResponse<T> { Data = result, IsSuccess = true }; } catch (HttpRequestException ex) { Console.Error.WriteLine($"API Request Failed: {ex.Message}"); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = ex.Message }; } catch (JsonException ex) { Console.Error.WriteLine($"JSON Deserialization Failed: {ex.Message}"); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = "Invalid JSON response." }; } catch (Exception ex) { Console.Error.WriteLine($"An unexpected error occurred: {ex.Message}"); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = "An unexpected error occurred." }; } } } // API Response Wrapper public class ApiResponse<T> { public T? Data { get; set; } public bool IsSuccess { get; set; } public string? ErrorMessage { get; set; } } // Usage in Application Logic (Dependency Injection) public class MyService { private readonly IApiClient _apiClient; public MyService(IApiClient apiClient) { _apiClient = apiClient; } public async Task<MyDataType?> GetDataFromApiAsync(string id, CancellationToken cancellationToken) { var response = await _apiClient.GetAsync<MyDataType>($"api/data/{id}", cancellationToken); if (response.IsSuccess) { return response.Data; } else { // Handle error (e.g., logging, retrying) Console.Error.WriteLine($"Error fetching data: {response.ErrorMessage}"); return null; } } } public class MyDataType { public string? SomeProperty { get; set; } public int SomeValue { get; set; } } """ **Anti-pattern:** Directly using "HttpClient" within application logic without wrapping it into a dedicated API client. This duplicates concerns and makes maintainability difficult. ### 1.2 Standard: Implement Retry Policies with Polly **Do This:** Use Polly, a .NET resilience and transient-fault-handling library, to implement retry policies for API calls. This helps handle transient network errors, temporary API unavailability, and rate limiting. **Don't Do This:** Implement ad-hoc retry logic that is inconsistent and difficult to manage. Don't retry indefinitely without a circuit breaker. **Why:** * **Reliability:** Mitigates the impact of transient errors, improving application robustness. * **Configuration:** Retry policies can be configured externally, allowing for adjustments without code changes. * **Centralization:** Polly policies can be applied consistently across all API calls. **Code Example:** """csharp using Polly; using Polly.Retry; using System.Net.Http; using System.Text.Json; using Polly.CircuitBreaker; public class ApiClient : IApiClient { private readonly HttpClient _httpClient; private readonly AsyncRetryPolicy _retryPolicy; private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy; public ApiClient(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); // Define a retry policy with exponential backoff. _retryPolicy = Policy .Handle<HttpRequestException>(ex => ex.StatusCode != System.Net.HttpStatusCode.NotFound) // Retries all HttpRequestExceptions excluding NotFound. .Or<TaskCanceledException>() // Also retries TaskCanceledException .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), // Exponential backoff onRetry: (exception, timespan, retryAttempt, context) => { Console.WriteLine($"Retry #{retryAttempt} due to: {exception.Message}, waiting {timespan}"); // Optionally log the retry attempt and reason }); // Define a circuit breaker policy to prevent overwhelming a failing service. _circuitBreakerPolicy = Policy .Handle<HttpRequestException>() // Breaks on any HttpRequestException. .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 3, durationOfBreak: TimeSpan.FromMinutes(1), onBreak: (exception, duration) => { Console.WriteLine($"Circuit breaker opened due to: {exception.Message}, duration: {duration}"); }, onReset: () => { Console.WriteLine("Circuit breaker reset."); }, onHalfOpen: () => { Console.WriteLine("Circuit breaker half-opened."); }); } public async Task<ApiResponse<T>> GetAsync<T>(string endpoint, CancellationToken cancellationToken) { try { // Wrap the API call with the retry and circuit breaker policies. return await _circuitBreakerPolicy.ExecuteAsync(async () => { return await _retryPolicy.ExecuteAsync(async () => { var response = await _httpClient.GetAsync(endpoint, cancellationToken); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(cancellationToken); return JsonSerializer.Deserialize<ApiResponse<T>>(content); }); }); } catch (HttpRequestException ex) { // Consider more specific error handling based on the type of exception. Console.Error.WriteLine($"API Request Failed: {ex.Message}"); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = ex.Message }; } catch (BrokenCircuitException) { // Circuit breaker is open Console.Error.WriteLine("Circuit breaker is open, request blocked."); return new ApiResponse<T> { IsSuccess = false, ErrorMessage = "Service unavailable (circuit breaker)." }; } } //Other methods follow a similar pattern } """ **Anti-pattern:** Manually implementing retry logic with "Thread.Sleep" without considering exponential backoff or circuit breakers. This can lead to resource exhaustion and decreased application stability when an API is experiencing issues. ### 1.3 Standard: Use Asynchronous Operations **Do This:** Perform all API calls asynchronously using "async" and "await". Configure "HttpClient" to avoid port exhaustion. **Don't Do This:** Use synchronous "HttpClient" methods ("Get", "Post", etc.) or "Task.Result"/"Task.Wait()" for API calls. Create a new "HttpClient" instance per API call. **Why:** * **Responsiveness:** Prevents blocking the main thread, ensuring the application remains responsive to user input. * **Scalability:** Allows the application to handle more concurrent requests without consuming excessive resources. * **Efficiency:** Asynchronous operations release threads while waiting for I/O, improving overall resource utilization. **Code Example:** """csharp public async Task<User?> GetUserAsync(int userId, CancellationToken cancellationToken) { var response = await _httpClient.GetAsync($"users/{userId}", cancellationToken); response.EnsureSuccessStatusCode(); // Ensure the request was successful var json = await response.Content.ReadAsStringAsync(cancellationToken); return JsonSerializer.Deserialize<User>(json); } """ **Anti-pattern:** Calling ".Result" or ".Wait()" on an async Task. This blocks the current thread and can lead to deadlocks in ASP.NET Core applications. It defeats the purpose of asynchrony. ### 1.4 Standard: Configure HttpClient Properly **Do This:** Use "IHttpClientFactory" to manage "HttpClient" instances. This avoids socket exhaustion and provides central configuration. Configure "HttpClient" with appropriate timeouts and default headers. **Don't Do This:** Create new "HttpClient" instances directly, especially in long-lived applications. **Why:** * **Socket Exhaustion:** Creating multiple "HttpClient" instances can lead to socket exhaustion, causing performance issues and connection errors. * **Centralized Configuration:** "IHttpClientFactory" allows for consistent configuration of "HttpClient" instances across the application. * **Lifecycle Management:** "IHttpClientFactory" handles the lifecycle of "HttpClient" instances, preventing resource leaks. **Code Example:** """csharp // Startup.cs or Program.cs builder.Services.AddHttpClient<IApiClient, ApiClient>(client => { client.BaseAddress = new Uri("https://api.example.com"); client.Timeout = TimeSpan.FromSeconds(30); client.DefaultRequestHeaders.Add("Accept", "application/json"); }); // ApiClient.cs (using Dependency Injection) public class ApiClient : IApiClient { private readonly HttpClient _httpClient; public ApiClient(HttpClient httpClient) { _httpClient = httpClient; } public async Task<ApiResponse<T>> GetAsync<T>(string endpoint, CancellationToken cancellationToken) { var response = await _httpClient.GetAsync(endpoint, cancellationToken); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(cancellationToken); return JsonSerializer.Deserialize<ApiResponse<T>>(content); } } """ **Anti-pattern:** Creating a new "HttpClient" directly with "new HttpClient()". This circumvents the benefits of "IHttpClientFactory". ## 2. Data Handling and Serialization Proper data handling and serialization are critical for efficient and reliable API integrations. ### 2.1 Standard: Use System.Text.Json for Serialization/Deserialization **Do This:** Use "System.Text.Json" for serializing and deserializing JSON data. It's the recommended JSON library in .NET, offering performance and security benefits. **Don't Do This:** Use older JSON libraries like "Newtonsoft.Json" (Json.NET) unless there are specific compatibility reasons or migration is infeasible. "Newtonsoft.Json" can introduce security vulnerabilities if misconfigured and "System.Text.Json" offers significantly better performance. **Why:** * **Performance:** "System.Text.Json" is optimized for performance and reduces memory allocation. * **Security:** Provides built-in protection against common JSON vulnerabilities. * **Modern .NET:** "System.Text.Json" is the standard library for JSON processing in modern .NET applications. **Code Example:** """csharp using System.Text.Json; public class User { public string? Name { get; set; } public int Age { get; set; } } // Serialization User user = new User { Name = "John Doe", Age = 30 }; string jsonString = JsonSerializer.Serialize(user); // Deserialization User? deserializedUser = JsonSerializer.Deserialize<User>(jsonString); """ **Anti-pattern:** Using overly complex custom serialization logic when "System.Text.Json" can handle the serialization automatically. ### 2.2 Standard: Define Data Transfer Objects (DTOs) **Do This:** Create DTOs that match the structure of the API's request and response payloads. Use attributes from "System.Text.Json" namespace to control the serialization process (e.g., property naming, ignoring properties). **Don't Do This:** Directly use domain entities as request or response payloads. Api contracts change independently from internal models. **Why:** * **Decoupling:** Changes to the API's data structure don't directly impact domain entities. * **Data Transformation:** Provides a clear separation for transforming data between the API and the application. * **Versioning:** DTOs can be versioned to handle API changes without affecting the core application logic. **Code Example:** """csharp using System.Text.Json.Serialization; public class UserDto { [JsonPropertyName("user_name")] // Map to API's property name public string? UserName { get; set; } [JsonIgnore] // Ignore this property during serialization/deserialization public string? InternalUseOnly { get; set; } public int Age { get; set; } } public class User { public string? Name { get; set; } public int Age { get; set; } } public static class UserMapper { public static User FromDto(UserDto dto) { return new User { Name = dto.UserName, Age = dto.Age }; } public static List<User> ListFromDto(List<UserDto> dtos) { return dtos.Select(FromDto).ToList(); } } """ **Anti-pattern:** Binding API responses directly to database entities. Exposes internal data structure and creates a strong dependency. ### 2.3 Standard: Handle Dates and Times Consistently **Do This:** Use "DateTimeOffset" for representing dates and times with timezone information. Specify a consistent format for serializing and deserializing dates (e.g., ISO 8601). Consider using a custom "JsonConverter" for specific datetime formats. **Don't Do This:** Use "DateTime" without considering timezone conversions. Rely on implicit date formatting. **Why:** * **Accuracy:** "DateTimeOffset" preserves timezone information, preventing misinterpretations. * **Interoperability:** ISO 8601 is a standard format for exchanging date and time data. **Code Example:** """csharp using System.Text.Json; using System.Text.Json.Serialization; public class Event { [JsonPropertyName("event_time")] public DateTimeOffset EventTime { get; set; } } // Custom converter for a specific date format public class CustomDateTimeConverter : JsonConverter<DateTimeOffset> { private readonly string _format; public CustomDateTimeConverter(string format) { _format = format; } public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return DateTimeOffset.ParseExact(reader.GetString()!, _format, null); } public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString(_format)); } } // Usage in JsonSerializerOptions var options = new JsonSerializerOptions { Converters = { new CustomDateTimeConverter("yyyy-MM-ddTHH:mm:ssZ") } }; """ **Anti-pattern:** Failing to handle timezone conversions properly, leading to incorrect date representations. ## 3. Security Considerations Security is paramount when integrating with APIs. ### 3.1 Standard: Secure API Keys and Secrets **Do This:** Store API keys and secrets securely using Azure Key Vault, Hashicorp Vault, or other secure storage mechanisms. Access these secrets at runtime using appropriate access controls. **Don't Do This:** Hardcode API keys and secrets in source code or configuration files. Check secrets into source control. **Why:** * **Protection:** Prevents unauthorized access to sensitive credentials. * **Centralized Management:** Provides a central location for managing and rotating secrets. **Code Example:** """csharp using Azure.Identity; using Azure.Security.KeyVault.Secrets; using Microsoft.Extensions.Configuration; public class ApiKeyProvider { private readonly SecretClient _secretClient; public ApiKeyProvider(IConfiguration configuration) { string keyVaultUri = configuration["KeyVaultUri"] ?? throw new ArgumentException("Missing KeyVaultUri configuration."); _secretClient = new SecretClient(new Uri(keyVaultUri), new DefaultAzureCredential()); } public async Task<string> GetApiKeyAsync(string secretName) { KeyVaultSecret secret = await _secretClient.GetSecretAsync(secretName); return secret.Value; } } // Usage ApiKeyProvider apiKeyProvider = new ApiKeyProvider(configuration); string apiKey = await apiKeyProvider.GetApiKeyAsync("MyApi-ApiKey"); """ **Anti-pattern:** Storing API keys in environment variables without proper encryption or access controls. ### 3.2 Standard: Validate API Responses **Do This:** Validate API responses to ensure data integrity and prevent unexpected errors. Check for expected data types, value ranges, and required fields. Implement schemas for response validation. **Don't Do This:** Blindly trust API responses without validation. **Why:** * **Data Integrity:** Protects against corrupted or malicious data. * **Error Prevention:** Catches errors early, preventing unexpected application behavior. * **Security:** Prevents injection attacks and other vulnerabilities. **Code Example:** """csharp public class ApiResponseValidator { public static bool ValidateUserResponse(User user) { if (string.IsNullOrEmpty(user.Name)) { Console.WriteLine("User name is missing."); return false; } if (user.Age < 0 || user.Age > 150) { Console.WriteLine("Invalid user age."); return false; } // Additional validation logic as needed return true; } } """ **Anti-pattern:** Assuming API responses always conform to the expected structure and data types. ### 3.3 Standard: Implement Input Sanitization and Output Encoding **Do This:** Sanitize user inputs before sending them to APIs to prevent injection attacks. Encode API outputs before displaying them to users to prevent cross-site scripting (XSS) vulnerabilities. **Don't Do This:** Trust user inputs without sanitization. Display API outputs directly without encoding. **Why:** * **Security:** Prevents injection attacks and XSS vulnerabilities. **Note:** Input sanitization and output encoding are general security best practices and also specifically applicable to API integration. Refer to OWASP guidelines for specific techniques. ## 4. Error Handling and Logging Effective error handling and logging are crucial for debugging and monitoring API integrations. ### 4.1 Standard: Implement Structured Logging **Do This:** Use a structured logging library like Serilog or NLog to log API requests, responses, and errors. Include relevant context information (e.g., request ID, timestamp, user ID) in log messages. **Don't Do This:** Use simple "Console.WriteLine" or "Debug.WriteLine" statements for logging. **Why:** * **Searchability:** Structured logs are easily searchable and analyzable. * **Debugging:** Provides detailed information for diagnosing issues. * **Monitoring:** Allows for monitoring API performance and identifying potential problems. **Code Example:** """csharp using Microsoft.Extensions.Logging; public class MyService { private readonly ILogger<MyService> _logger; private readonly IApiClient _apiClient; public MyService(ILogger<MyService> logger, IApiClient apiClient) { _logger = logger; _apiClient = apiClient; } public async Task<MyDataType?> GetDataFromApiAsync(string id, CancellationToken cancellationToken) { _logger.LogInformation("Getting data from API for ID: {Id}", id); try { var response = await _apiClient.GetAsync<MyDataType>($"api/data/{id}", cancellationToken); if (response.IsSuccess) { _logger.LogDebug("Successfully retrieved data from API for ID: {Id}", id); return response.Data; } else { _logger.LogError("Error fetching data from API for ID: {Id}. Error: {ErrorMessage}", id, response.ErrorMessage); return null; } } catch (Exception ex) { _logger.LogError(ex, "An unexpected error occurred while fetching data from API for ID: {Id}", id); return null; } } } """ **Anti-pattern:** Logging only error messages without sufficient context information. ### 4.2 Standard: Handle Exceptions Gracefully **Do This:** Catch API-related exceptions (e.g., "HttpRequestException", "JsonException") and handle them gracefully. Provide informative error messages to the user. Log the exception details for debugging purposes. **Don't Do This:** Rethrow exceptions without adding context. Ignore exceptions. **Why:** * **User Experience:** Prevents the application from crashing or displaying cryptic error messages. * **Debugging:** Provides valuable information for diagnosing and resolving issues. **Code Example:** See error handling examples in the provided snippets in previous sections. **Anti-pattern:** Displaying raw exception messages directly to the user. ## 5. Design Pattern Implementation ### 5.1. Repository Pattern **Do This:** Abstract data access logic behind repositories. This approach allows for easier swapping of API sources and testability. **Don't Do This:** Directly call API methods in your business logic components. **Why:** Decoupling data access from business logic makes the system more flexible and maintainable. """csharp public interface IUserRepository { Task<User?> GetUserAsync(int id); Task<List<User>> GetAllUsersAsync(); } public class UserRepository : IUserRepository { private readonly IApiClient _apiClient; private const string EndpointBase = "users"; public UserRepository(IApiClient apiClient) { _apiClient = apiClient; } public async Task<User?> GetUserAsync(int id) { var response = await _apiClient.GetAsync<User>($"{EndpointBase}/{id}", default); if (response.IsSuccess && response.Data != null) { return response.Data; } return null; } public async Task<List<User>> GetAllUsersAsync() { var response = await _apiClient.GetAsync<List<User>>(EndpointBase, default); if (response.IsSuccess && response.Data != null) { return response.Data; } return new List<User>(); } } """ ## 6. C# Language Feature Usage ### 6.1. Record Types **Do This:** Leverage record types for DTOs where immutability is desired. Records provide concise syntax and built-in value equality. """csharp public record UserDto(string UserName, int Age); """ ### 6.2. Nullable Reference Types **Do This:** Use nullable reference types ("string?", "object?") to explicitly indicate whether a property or variable can be null. This improves null-safety and helps prevent "NullReferenceException" errors. """csharp public class User { public string? Name { get; set; } // Indicates that Name can be null public ContactInfo Contact { get; set; } = new ContactInfo(); //Non-nullable, initialized } """ ## 7. Conclusion These coding standards provide a comprehensive guide to integrating with APIs in C# applications. By adhering to these guidelines, developers can create maintainable, performant, and secure API integrations that meet the demands of modern software development. This document's recommendations should be considered mandatory for any professional team using C# and integrating with external services.