| name | dotnet-csharp |
| description | Universal C# / .NET 10 standards that apply to ANY .NET project regardless of architecture or layer. Covers syntax, usings, naming, async patterns, null safety, and DTO conventions. ALWAYS load this skill for ANY agent that writes C# code. DO NOT load for tasks that only read or analyze existing code without writing.
|
| requires | [] |
| produces | ["c#-conventions","naming","async-patterns","null-safety"] |
C# Universal Standards — TemperAI
🚨 NON-NEGOTIABLE RULES — ZERO TOLERANCE
The following rules have ZERO tolerance for violations.
Code that breaks ANY of these will be rejected immediately:
- DTOs MUST be
sealed record with explicit properties — NEVER class, NEVER primary constructors
- NEVER magic strings — use enums or constants for ALL repeated strings
- NEVER named usings (
using X = Y;) — rename the type instead
- NEVER
! null-forgiving operator — fix your validation logic
- NEVER
async void — always return Task
If you're unsure about a rule, ASK before writing code. Do NOT assume "it's probably fine."
These standards apply to ALL C# code in TemperAI projects: backend, frontend, and tests.
Never duplicate these rules in architecture-specific or layer-specific skills.
When NOT to apply this skill
- You are only reading or analyzing existing code without writing new code
- You are writing configuration files (JSON, YAML, XML) — not C#
Syntax & Structure
1. Never primary constructors
public class Product(string name) { }
public class Product
{
public Product(string name) { ... }
}
2. Never expression-bodied methods for bodies
public string GetName() => Name;
public string GetName()
{
return Name;
}
3. Never break short lines unnecessarily
Result<ProductDto> result =
await handler.HandleAsync(req, ct);
Result<ProductDto> result = await handler.HandleAsync(req, ct);
4. Always organize usings before namespace
using System;
namespace P;
using System.Collections;
class X {}
using System;
using System.Collections;
namespace P;
class X {}
Imports & Usings
5. Never using static
using static System.Console;
WriteLine("Hi");
using System;
Console.WriteLine("Hi");
6. NEVER named usings — rename the type instead
NEVER use using X = Y;. If you have a name collision, rename the conflicting class itself.
using ProductEntity = MyApp.Domain.Entities.Product;
using DomainTask = TodoApp.Domain.Task;
using MyApp.Domain.Entities;
Product product = new();
Collision resolution — rename at the source:
public class Task { ... }
public class WorkItem { ... }
Rules:
- ZERO named usings — not even "just this once"
- Rename conflicting types at the source
- File names must match class names exactly
7. Never global usings
global using System;
using System;
8. Never fully qualified type names in code
public Projects.Enums.Status Status { get; set; }
FluentValidation.Results.ValidationResult result = ...;
System.Collections.Generic.List<string> list = ...;
using Projects.Enums;
using FluentValidation.Results;
public Status Status { get; set; }
ValidationResult result = ...;
List<string> list = ...;
Naming & Conventions
9. Always use explicit types — never var
var result = await _service.GetAsync(id);
ProductDto result = await _service.GetAsync(id);
10. Entity folders always plural
Domain/Product/Product.cs
Domain/Products/Product.cs
11. Always write code in English
public bool EstaActivo { get; set; }
public bool IsActive { get; set; }
12. Always sealed on non-inherited classes
public class ProductRepository { }
public sealed class ProductRepository { }
13. NEVER magic strings — ALWAYS use constants or enums
if (product.Status == "active") { ... }
string connection = Configuration["ConnectionStrings:Default"];
public enum ProductStatus { Active, Inactive, Pending }
if (product.Status == ProductStatus.Active) { ... }
public static class ConfigKeys
{
public const string DefaultConnection = "ConnectionStrings:Default";
}
string connection = Configuration[ConfigKeys.DefaultConnection];
Decision rule:
- State / status / type → enum
- Config key / route / repeated literal →
const string
- Same string appears twice → extract to constant immediately
Null Safety
14. Never use the null-forgiving operator (!)
The ! operator MUST NEVER BE USED. If you feel you need it, your validation logic is wrong.
(List<string> errors, TodoItem? item) = TodoItem.Create(request.Title);
if (errors.Count > 0)
{
return Result<TodoItemDto>.Failure(HttpStatusCode.BadRequest).WithErrors(errors);
}
await _repo.AddAsync(item!, ct);
(List<string> errors, TodoItem? item) = TodoItem.Create(request.Title);
if (item is null)
{
return Result<TodoItemDto>.Failure(HttpStatusCode.BadRequest).WithErrors(errors);
}
await _repo.AddAsync(item, ct);
Rules:
- Never use
! — fix validation logic instead
- Always validate
if (entity is null) — not if (errors.Count > 0)
- Factory methods return
(List<string> errors, T? entity) — check the entity, not the list
Async & Threading
15. Never async void
public async void DoWork() { }
public async Task DoWork() { }
16. Never .Result or .Wait()
Product product = _service.Get(id).Result;
Product product = await _service.Get(id);
17. Always CancellationToken on public async methods
public async Task<Product> GetByIdAsync(Guid id) { }
public async Task<Product> GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { }
18. Never throw exceptions — use Result pattern
if (!IsValid) throw new ValidationException("Invalid");
if (!IsValid) return Result<T>.Failure(HttpStatusCode.BadRequest).WithErrors(["Invalid"]);
Rules:
- Never throw in application code — only unhandled infrastructure errors reach the global handler
- Never create custom exceptions — Result pattern handles all error flows
- Always use
Result<T>.Success() / Result<T>.Failure() with appropriate HTTP status codes
DTOs
19. DTOs must be sealed record with explicit properties and Dto suffix
public class CreateProductRequest(string name, decimal price) { }
public sealed record CreateProductRequestDto
{
public required string Name { get; init; }
public decimal Price { get; init; }
}
Quick rules:
- Always
sealed record with explicit properties
- Always
Dto suffix — CreateProductRequestDto, ProductResponseDto
- Request DTOs:
{ get; init; } — allows object initializers
- Non-nullable DTO properties use
required instead of !
- Use nullable properties only when null is semantically valid
- File name must match DTO name exactly
For complete DTO rules, naming, mapping patterns, and anti-patterns:
→ load backend/dotnet/shared/dto-conventions/SKILL.md