microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.3.4

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/instructions/csharp/csharp.instructions.md

366lines · modecode

1---
2applyTo: '**/*.cs'
3description: 'Required instructions for C# (CSharp) research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core'
4---
5# C# Instructions
6
7Conventions for C# development targeting .NET 10 and C# 14.
8
9## Project Structure
10
11Solutions follow a standard folder structure:
12
13```text
14Solution.sln
15Dockerfile
16src/
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
29Project 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
51Omit 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
60Add project-wide global usings:
61
62```xml
63<ItemGroup>
64 <Using Include="System.Text.Json" />
65</ItemGroup>
66```
67
68Use `Directory.Build.props` for shared configuration across multi-project solutions.
69
70## Managing Projects
71
72Essential `dotnet` CLI commands:
73
74```bash
75dotnet new list # Available templates
76dotnet new xunit -n Project.Tests # Create from template
77dotnet sln add ./src/Project/Project.csproj # Add to solution
78dotnet add reference ./src/Shared/Shared.csproj
79dotnet add package Newtonsoft.Json --version 13.0.3
80dotnet build && dotnet test
81```
82
83Reuse 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
100Member ordering:
101
1021. `const` → `static readonly` → `readonly` → instance fields
1032. Constructors
1043. Properties
1054. Methods
106
107Within categories, order: `public` → `protected` → `private` → `internal`.
108
109Access modifier keyword order: `[access] [static] [readonly] [async] [override|virtual|abstract] [partial]`
110
111### Variable Declarations and Primary Constructors
112
113Use `var` when type is obvious from the right side. Use target-typed `new()` when type is declared on left:
114
115```csharp
116var service = new UserService();
117Dictionary<string, int> lookup = new();
118```
119
120Primary constructors are preferred when initialization is straightforward:
121
122```csharp
123public class UserService(ILogger<UserService> logger, IRepository repo)
124{
125 public void Process() => logger.LogInformation("Processing");
126}
127```
128
129Use traditional constructors when validation runs before assignment or multiple overloads exist.
130
131Collection expressions: `int[] nums = [1, 2, 3];` and spread: `[..existing, 4, 5]`.
132
133Prefer early returns over deep nesting.
134
135## Code Documentation
136
137Public and protected members require XML documentation.
138
139Guidelines:
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>
155public 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>
210public 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
223Interface implementations use `<inheritdoc/>`:
224
225```csharp
226public 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
234public class UpperCaseProcessor : IProcessor
235{
236 /// <inheritdoc/>
237 public string Process(string input) => input.ToUpperInvariant();
238}
239```
240
241## Namespaces
242
243File-scoped namespaces are preferred:
244
245```csharp
246namespace Company.Project.Feature;
247
248public class Example { }
249```
250
251Namespaces align with folder structure.
252
253## Nullable Reference Types
254
255Enable 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
265Avoid `!` 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
272if (!dict.TryGetValue(key, out var value))
273 throw new KeyNotFoundException(key);
274return 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
286Demonstrates naming, structure, generics, primary constructors, nullable annotations, access modifier ordering, `Lock` type, and `field` keyword:
287
288```csharp
289namespace Company.Project.Widgets;
290
291using ItemCache = Dictionary<string, object>;
292
293/// <summary>Defines folding behavior for widgets.</summary>
294public interface IWidget
295{
296 Task StartFoldingAsync(CancellationToken cancellationToken);
297}
298
299/// <summary>Base for widgets processing data into collections.</summary>
300public 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>
337public 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