microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.2.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

367lines · modecode

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