microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
.github/instructions/csharp/csharp.instructions.md
366lines · modecode
| 1 | --- |
| 2 | applyTo: '**/*.cs' |
| 3 | description: 'Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core' |
| 4 | --- |
| 5 | # C# Instructions |
| 6 | |
| 7 | Conventions for C# development targeting .NET 10 and C# 14. |
| 8 | |
| 9 | ## Project Structure |
| 10 | |
| 11 | Solutions follow a standard folder structure: |
| 12 | |
| 13 | ```text |
| 14 | Solution.sln |
| 15 | Dockerfile |
| 16 | src/ |
| 17 | Project/ |
| 18 | Project.csproj |
| 19 | Program.cs |
| 20 | Project.Tests/ |
| 21 | Project.Tests.csproj |
| 22 | ``` |
| 23 | |
| 24 | * `.sln` and `Dockerfile` at repository root |
| 25 | * `src/` contains all project directories |
| 26 | * Project directories match `.csproj` names |
| 27 | * Test projects use `*.Tests` suffix |
| 28 | |
| 29 | Project folder organization scales with complexity. Keep all files at root when fewer than 16 files exist. When folders become necessary, prefer DDD-style names: `Application`, `Domain`, `Infrastructure`, `Services`, `Repositories`, `Controllers`. |
| 30 | |
| 31 | ## Project Configuration |
| 32 | |
| 33 | ### Target Framework |
| 34 | |
| 35 | | Target | TFM | Use Case | |
| 36 | |-------------------|----------------------|-----------------------------------| |
| 37 | | Cross-platform | `net10.0` | Console apps, libraries, web APIs | |
| 38 | | Windows-specific | `net10.0-windows` | WinForms, WPF | |
| 39 | | Android/iOS/macOS | `net10.0-{platform}` | Mobile and desktop | |
| 40 | |
| 41 | ```xml |
| 42 | <Project Sdk="Microsoft.NET.Sdk"> |
| 43 | <PropertyGroup> |
| 44 | <TargetFramework>net10.0</TargetFramework> |
| 45 | <ImplicitUsings>enable</ImplicitUsings> |
| 46 | <Nullable>enable</Nullable> |
| 47 | </PropertyGroup> |
| 48 | </Project> |
| 49 | ``` |
| 50 | |
| 51 | Omit explicit `LangVersion` as .NET 10 defaults to C# 14. Avoid `LangVersion=latest`. |
| 52 | |
| 53 | ### Implicit Usings |
| 54 | |
| 55 | | SDK | Implicit Namespaces | |
| 56 | |-------------------------|----------------------------------------------------------------------------------------------| |
| 57 | | `Microsoft.NET.Sdk` | `System`, `System.Collections.Generic`, `System.IO`, `System.Linq`, `System.Threading.Tasks` | |
| 58 | | `Microsoft.NET.Sdk.Web` | Base plus `Microsoft.AspNetCore.*`, `Microsoft.Extensions.*` | |
| 59 | |
| 60 | Add project-wide global usings: |
| 61 | |
| 62 | ```xml |
| 63 | <ItemGroup> |
| 64 | <Using Include="System.Text.Json" /> |
| 65 | </ItemGroup> |
| 66 | ``` |
| 67 | |
| 68 | Use `Directory.Build.props` for shared configuration across multi-project solutions. |
| 69 | |
| 70 | ## Managing Projects |
| 71 | |
| 72 | Essential `dotnet` CLI commands: |
| 73 | |
| 74 | ```bash |
| 75 | dotnet new list # Available templates |
| 76 | dotnet new xunit -n Project.Tests # Create from template |
| 77 | dotnet sln add ./src/Project/Project.csproj # Add to solution |
| 78 | dotnet add reference ./src/Shared/Shared.csproj |
| 79 | dotnet add package Newtonsoft.Json --version 13.0.3 |
| 80 | dotnet build && dotnet test |
| 81 | ``` |
| 82 | |
| 83 | Reuse existing package versions when adding packages already present in the solution. |
| 84 | |
| 85 | ## Coding Conventions |
| 86 | |
| 87 | ### Naming |
| 88 | |
| 89 | | Element | Convention | Example | |
| 90 | |--------------------|------------------|-----------------------| |
| 91 | | Classes/Files | `PascalCase` | `UserService.cs` | |
| 92 | | Interfaces | `IPascalCase` | `IRepository` | |
| 93 | | Methods/Properties | `PascalCase` | `ProcessAsync` | |
| 94 | | Fields | `camelCase` | `_logger`, `isActive` | |
| 95 | | Base classes | `PascalCaseBase` | `WidgetBase` | |
| 96 | | Type parameters | `TName` | `TEntity` | |
| 97 | |
| 98 | ### Class Structure |
| 99 | |
| 100 | Member ordering: |
| 101 | |
| 102 | 1. `const` → `static readonly` → `readonly` → instance fields |
| 103 | 2. Constructors |
| 104 | 3. Properties |
| 105 | 4. Methods |
| 106 | |
| 107 | Within categories, order: `public` → `protected` → `private` → `internal`. |
| 108 | |
| 109 | Access modifier keyword order: `[access] [static] [readonly] [async] [override|virtual|abstract] [partial]` |
| 110 | |
| 111 | ### Variable Declarations and Primary Constructors |
| 112 | |
| 113 | Use `var` when type is obvious from the right side. Use target-typed `new()` when type is declared on left: |
| 114 | |
| 115 | ```csharp |
| 116 | var service = new UserService(); |
| 117 | Dictionary<string, int> lookup = new(); |
| 118 | ``` |
| 119 | |
| 120 | Primary constructors are preferred when initialization is straightforward: |
| 121 | |
| 122 | ```csharp |
| 123 | public class UserService(ILogger<UserService> logger, IRepository repo) |
| 124 | { |
| 125 | public void Process() => logger.LogInformation("Processing"); |
| 126 | } |
| 127 | ``` |
| 128 | |
| 129 | Use traditional constructors when validation runs before assignment or multiple overloads exist. |
| 130 | |
| 131 | Collection expressions: `int[] nums = [1, 2, 3];` and spread: `[..existing, 4, 5]`. |
| 132 | |
| 133 | Prefer early returns over deep nesting. |
| 134 | |
| 135 | ## Code Documentation |
| 136 | |
| 137 | Public and protected members require XML documentation. |
| 138 | |
| 139 | Guidelines: |
| 140 | |
| 141 | * Use `<see cref="..."/>` for inline type and member references |
| 142 | * Use `<inheritdoc/>` on implementations and overrides |
| 143 | * Use `<inheritdoc cref="..."/>` when default resolution is insufficient |
| 144 | * Document exceptions with `<exception cref="...">` when methods throw |
| 145 | * Include `<param>` for all parameters and `<returns>` for non-void methods |
| 146 | |
| 147 | ```csharp |
| 148 | /// <summary> |
| 149 | /// Provides operations for managing user accounts. |
| 150 | /// </summary> |
| 151 | /// <remarks> |
| 152 | /// This service requires a configured <see cref="IUserRepository"/> and validates |
| 153 | /// all inputs before persistence. Thread-safe for concurrent access. |
| 154 | /// </remarks> |
| 155 | public class UserService(IUserRepository repository, ILogger<UserService> logger) |
| 156 | { |
| 157 | /// <summary> |
| 158 | /// Retrieves a user by their unique identifier. |
| 159 | /// </summary> |
| 160 | /// <param name="userId">The unique identifier of the user to retrieve.</param> |
| 161 | /// <param name="cancellationToken">Token to cancel the operation.</param> |
| 162 | /// <returns> |
| 163 | /// The <see cref="User"/> if found; otherwise, <see langword="null"/>. |
| 164 | /// </returns> |
| 165 | /// <exception cref="ArgumentException"> |
| 166 | /// Thrown when <paramref name="userId"/> is empty or whitespace. |
| 167 | /// </exception> |
| 168 | public async Task<User?> GetUserAsync(string userId, CancellationToken cancellationToken = default) |
| 169 | { |
| 170 | ArgumentException.ThrowIfNullOrWhiteSpace(userId); |
| 171 | return await repository.FindByIdAsync(userId, cancellationToken); |
| 172 | } |
| 173 | |
| 174 | /// <summary> |
| 175 | /// Creates a new user with the specified details. |
| 176 | /// </summary> |
| 177 | /// <param name="name">The display name for the user.</param> |
| 178 | /// <param name="email">The email address for the user.</param> |
| 179 | /// <param name="cancellationToken">Token to cancel the operation.</param> |
| 180 | /// <returns>The created <see cref="User"/> with assigned identifier.</returns> |
| 181 | /// <exception cref="InvalidOperationException"> |
| 182 | /// Thrown when a user with the same <paramref name="email"/> already exists. |
| 183 | /// </exception> |
| 184 | /// <example> |
| 185 | /// <code> |
| 186 | /// var user = await userService.CreateUserAsync("Jane Doe", "jane@example.com"); |
| 187 | /// Console.WriteLine($"Created user: {user.Id}"); |
| 188 | /// </code> |
| 189 | /// </example> |
| 190 | public async Task<User> CreateUserAsync( |
| 191 | string name, |
| 192 | string email, |
| 193 | CancellationToken cancellationToken = default) |
| 194 | { |
| 195 | var existing = await repository.FindByEmailAsync(email, cancellationToken); |
| 196 | if (existing is not null) |
| 197 | throw new InvalidOperationException($"User with email '{email}' already exists."); |
| 198 | |
| 199 | var user = new User { Name = name, Email = email }; |
| 200 | await repository.AddAsync(user, cancellationToken); |
| 201 | logger.LogInformation("Created user {UserId}", user.Id); |
| 202 | return user; |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | /// <summary> |
| 207 | /// Represents a user account in the system. |
| 208 | /// </summary> |
| 209 | /// <typeparam name="TMetadata">The type of additional metadata associated with the user.</typeparam> |
| 210 | public class User<TMetadata> where TMetadata : class |
| 211 | { |
| 212 | /// <summary>Gets or sets the unique identifier.</summary> |
| 213 | public required string Id { get; set; } |
| 214 | |
| 215 | /// <summary>Gets or sets the display name.</summary> |
| 216 | public required string Name { get; set; } |
| 217 | |
| 218 | /// <summary>Gets or sets optional metadata.</summary> |
| 219 | public TMetadata? Metadata { get; set; } |
| 220 | } |
| 221 | ``` |
| 222 | |
| 223 | Interface implementations use `<inheritdoc/>`: |
| 224 | |
| 225 | ```csharp |
| 226 | public interface IProcessor |
| 227 | { |
| 228 | /// <summary>Processes the input and returns the result.</summary> |
| 229 | /// <param name="input">The input to process.</param> |
| 230 | /// <returns>The processed result.</returns> |
| 231 | string Process(string input); |
| 232 | } |
| 233 | |
| 234 | public class UpperCaseProcessor : IProcessor |
| 235 | { |
| 236 | /// <inheritdoc/> |
| 237 | public string Process(string input) => input.ToUpperInvariant(); |
| 238 | } |
| 239 | ``` |
| 240 | |
| 241 | ## Namespaces |
| 242 | |
| 243 | File-scoped namespaces are preferred: |
| 244 | |
| 245 | ```csharp |
| 246 | namespace Company.Project.Feature; |
| 247 | |
| 248 | public class Example { } |
| 249 | ``` |
| 250 | |
| 251 | Namespaces align with folder structure. |
| 252 | |
| 253 | ## Nullable Reference Types |
| 254 | |
| 255 | Enable at project level with `<Nullable>enable</Nullable>`. |
| 256 | |
| 257 | ### Annotations |
| 258 | |
| 259 | * Use `?` for nullable types: `string? GetName()` |
| 260 | * Use `[NotNull]`, `[MaybeNull]`, `[NotNullWhen(bool)]` for complex scenarios |
| 261 | * Prefer `required` modifier for non-nullable properties without defaults |
| 262 | |
| 263 | ### Null-Forgiving Operator |
| 264 | |
| 265 | Avoid `!` except when: |
| 266 | |
| 267 | * Framework APIs lack nullable annotations |
| 268 | * Test code asserts non-null conditions |
| 269 | * Preceding validation guarantees non-null |
| 270 | |
| 271 | ```csharp |
| 272 | if (!dict.TryGetValue(key, out var value)) |
| 273 | throw new KeyNotFoundException(key); |
| 274 | return value!.ToUpper(); |
| 275 | ``` |
| 276 | |
| 277 | ## Additional Conventions |
| 278 | |
| 279 | * Prefer `Span<T>` and `ReadOnlySpan<T>` for array operations |
| 280 | * Use `out var` pattern: `dict.TryGetValue("key", out var value)` |
| 281 | * Use `System.Threading.Lock` with `EnterScope()` for synchronization |
| 282 | * Omit types on lambda parameters |
| 283 | |
| 284 | ## Complete Example |
| 285 | |
| 286 | Demonstrates naming, structure, generics, primary constructors, nullable annotations, access modifier ordering, `Lock` type, and `field` keyword: |
| 287 | |
| 288 | ```csharp |
| 289 | namespace Company.Project.Widgets; |
| 290 | |
| 291 | using ItemCache = Dictionary<string, object>; |
| 292 | |
| 293 | /// <summary>Defines folding behavior for widgets.</summary> |
| 294 | public interface IWidget |
| 295 | { |
| 296 | Task StartFoldingAsync(CancellationToken cancellationToken); |
| 297 | } |
| 298 | |
| 299 | /// <summary>Base for widgets processing data into collections.</summary> |
| 300 | public abstract class WidgetBase<TData, TCollection>( |
| 301 | ILogger logger, |
| 302 | IReadOnlyList<string> prefixes) |
| 303 | where TData : class |
| 304 | where TCollection : IEnumerable<TData> |
| 305 | { |
| 306 | protected static readonly int DefaultProcessCount = 10; |
| 307 | protected readonly ILogger Logger = logger; |
| 308 | private readonly Lock _lock = new(); |
| 309 | private readonly IReadOnlyList<string> _prefixes = prefixes; |
| 310 | |
| 311 | protected int nextProcess; |
| 312 | |
| 313 | public IReadOnlyList<string> Prefixes => _prefixes; |
| 314 | |
| 315 | public string? LastProcessedId |
| 316 | { |
| 317 | get => field; |
| 318 | protected set => field = value?.Trim(); |
| 319 | } |
| 320 | |
| 321 | public int ApplyFold(TData item) |
| 322 | { |
| 323 | if (item is null) return 0; |
| 324 | |
| 325 | using (_lock.EnterScope()) |
| 326 | { |
| 327 | var folds = ProcessFold(item); |
| 328 | nextProcess += [..folds].Count; |
| 329 | return nextProcess; |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | protected abstract TCollection ProcessFold(TData item); |
| 334 | } |
| 335 | |
| 336 | /// <summary>Widget using stack-based collection.</summary> |
| 337 | public class StackWidget<TData>( |
| 338 | ILogger<StackWidget<TData>> logger, |
| 339 | IRepository<TData> repository) |
| 340 | : WidgetBase<TData, Stack<TData>>(logger, ["first", "second"]), IWidget |
| 341 | where TData : class |
| 342 | { |
| 343 | private readonly IRepository<TData> _repository = repository; |
| 344 | |
| 345 | /// <inheritdoc/> |
| 346 | public async Task StartFoldingAsync(CancellationToken cancellationToken) |
| 347 | { |
| 348 | if (cancellationToken.IsCancellationRequested) return; |
| 349 | |
| 350 | var items = await _repository.GetAllAsync(cancellationToken); |
| 351 | foreach (var item in items) |
| 352 | ApplyFold(item); |
| 353 | |
| 354 | Logger.LogInformation("Processed {Count} items", nextProcess); |
| 355 | } |
| 356 | |
| 357 | /// <inheritdoc/> |
| 358 | protected override Stack<TData> ProcessFold(TData item) |
| 359 | { |
| 360 | Stack<TData> result = new(); |
| 361 | result.Push(item); |
| 362 | LastProcessedId = item.GetHashCode().ToString(); |
| 363 | return result; |
| 364 | } |
| 365 | } |
| 366 | ``` |
| 367 | |