backend-conventions
Skill234 starsChanged 3 months agoMITView on GitHub
Backend convention reference (.NET 10 / C# 13). Auto-injected into backend-aware agents - not user-invocable.
6 other projects use a version of this.
Adopted by
Most recently taken up by
- jbouder/skillstheir own version, first seen Nov 1, 2026
- flntfnd/claude-skillstheir own version, first seen Aug 20, 2026
- ryokkon624/scrum-agent-basetheir own version, first seen Jul 9, 2026
- getsentry/sentrytheir own version, first seen Jun 30, 2026
- sefaertunc/anthropic-watchtheir own version, first seen Apr 30, 2026
- sefaertunc/Worclaudetheir own version, first seen Apr 28, 2026
Other versions6 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 6
- getsentry/sentry★ 46ksame name
- sefaertunc/Worclaude★ 4same name
- jbouder/skills★ 1same name
- sefaertunc/anthropic-watch★ 1same name
- flntfnd/claude-skills★ 0same name
- ryokkon624/scrum-agent-base★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository29 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- add-aspire-depSkill
- add-background-jobSkill
- add-ci-areaSkill
- add-email-templateSkill
- add-env-varSkill
- add-options-classSkill
- add-permissionSkill
- add-rate-limitSkill
- address-reviewSkill
- add-testSkill
- create-issueSkill
- create-prSkill
- create-releaseSkill
- frontend-conventionsSkill
- gen-typesSkill
- infra-conventionsSkill
- init-verifySkill
- manage-file-storageSkill
- new-endpointSkill
- new-entitySkill
- new-featureSkill
- new-pageSkill
- polish-uiSkill
- review-dependabotSkill
- review-designSkill
- review-prSkill
- security-conventionsSkill
- verifySkill
The file
We split this skill into the rules it gives an agent
9 rules. 0 are common, 0 are distinctive, and 9 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
0
Distinctive
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 44
Collections: Prefer IReadOnlyList<T> on public interfaces.
- Very specific: line 69
Always serialize metadata with JsonSerializer.Serialize - never string interpolation (JSON injection risk).
- Very specific: line 75
Default public schema. Named schemas only for existing grouped features (e.g., "auth").
- Very specific: line 134
Never /// <param name="cancellationToken"> - it leaks into OAS requestBody.description
- Very specific: line 156
Every ProblemDetails response carries the code in the code extension (ProblemFactory for controllers/middleware, ProblemFactory.EnsureCode …
- Very specific: line 170
Never class-level [Authorize(Roles)] on controllers using permissions
- Very specific: line 200
Read to byte[] in the controller: using var ms = new MemoryStream().
- Very specific: line 204
Storage keys: Use {feature}/{id}.{ext} pattern (e.g., avatars/{userId}.webp).
- Very specific: line 247
Run: dotnet run --project src/backend/MyProject.AppHost - launches PostgreSQL, MinIO, MailPit, API, and Frontend.
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Backend Conventions (.NET 10 / C# 13)
Project Structure
src/backend/
├── MyProject.Shared/ # Result, Error, ErrorType, ErrorMessages (zero deps)
├── MyProject.Domain/Entities/ # Business entities (BaseEntity)
├── MyProject.Application/ # Interfaces, DTOs, service contracts
│ ├── Features/{Feature}/I{Feature}Service.cs
│ ├── Features/{Feature}/Dtos/{Operation}Input.cs, {Entity}Output.cs
│ ├── Identity/IUserService.cs, IUserContext.cs
│ └── Identity/Constants/AppRoles.cs, AppPermissions.cs
├── MyProject.Infrastructure/ # Implementations (all internal)
│ ├── Features/{Feature}/Services/, Configurations/, Extensions/
│ └── Persistence/MyProjectDbContext.cs
├── MyProject.ServiceDefaults/ # Aspire shared: OTEL, service discovery, resilience
├── MyProject.AppHost/ # Aspire orchestrator (local dev only)
└── MyProject.WebApi/ # Entry point
├── Features/{Feature}/{Feature}Controller.cs, {Feature}Mapper.cs
├── Features/{Feature}/Dtos/{Operation}/{Operation}Request.cs + Validator
├── Authorization/ # RequirePermission, PermissionPolicyProvider
└── Shared/ # ApiController, ProblemFactory, ValidationConstantsC# Conventions
Access Modifiers
| Item | Modifier |
| Domain entities, Application interfaces/DTOs | public |
| Infrastructure services, EF configs, mappers | internal |
| WebApi controllers, request/response DTOs | public |
Key Rules
- Nullability:
string.Emptyfor required,string?for optional. Express nullability in the type system. - Collections: Prefer
IReadOnlyList<T>on public interfaces. Avoid exposingList<T>orT[]directly. - Time:
TimeProviderregistered asTimeProvider.Systemsingleton. - NuGet: To add a package:
<PackageVersion Include="Pkg" Version="X.Y.Z" />inDirectory.Packages.props,<PackageReference Include="Pkg" />in.csproj.
Entity Definition
New entities should extend BaseEntity (provides Id, CreatedAt/By, UpdatedAt/By, IsDeleted, DeletedAt/By - all set by AuditingInterceptor automatically) and use BaseEntityRepository<T> for data access.
Rules:
- Private setters, enforce invariants through methods
- Protected parameterless ctor for EF Core
- Derived entity ctor sets
Id = Guid.NewGuid()(viaprotected init) - Boolean naming:
Is*/Has*in C#, prefix-free column names viaHasColumnName - Soft delete via
entity.SoftDelete()/entity.Restore()- never setIsDeleteddirectly
Audit Trail
AuditEvent is append-only, does NOT extend BaseEntity. No FK on UserId (users are hard-deleted). Fire-and-forget logging - failures never break operations.
await auditService.LogAsync(AuditActions.AdminAssignRole, userId: callerId,
targetEntityType: "User", targetEntityId: targetId,
metadata: JsonSerializer.Serialize(new { role = input.Role }), ct: cancellationToken);Always serialize metadata with JsonSerializer.Serialize - never string interpolation (JSON injection risk).
EF Core
Configurations inherit BaseEntityConfiguration<T> (public abstract), override ConfigureEntity. Mark derived configurations internal. Auto-discovered via ApplyConfigurationsFromAssembly().
- Default
publicschema. Named schemas only for existing grouped features (e.g.,"auth"). .HasComment()on all enum columns documenting values.- Seeding: roles and their default permissions via
AppRoles.Definitions(declarative, idempotent upsert inSeedRolesAsync()).
Migration command:
dotnet ef migrations add {Name} \
--project src/backend/MyProject.Infrastructure \
--startup-project src/backend/MyProject.WebApi \
--output-dir Persistence/MigrationsResult Pattern
// Success
return Result<Guid>.Success(entity.Id);
// Static error (code + message) - always an ErrorMessages entry
return Result.Failure(ErrorMessages.Admin.UserNotFound, ErrorType.NotFound);
// Runtime values go in server-side logs, never in client responses
logger.LogWarning("Operation failed for user '{UserId}': {Errors}", userId, errors);
return Result.Failure(ErrorMessages.Admin.DeleteFailed);| ErrorType | HTTP | When |
| (omit - default) | 400 | Validation / business rule failures |
ErrorType.Unauthorized | 401 | Auth / token failures |
ErrorType.Forbidden | 403 | Authenticated but insufficient privileges |
ErrorType.NotFound | 404 | Entity not found |
Controller: ProblemFactory.Create(result.Error, result.ErrorType) for failures. result.Error is an Error record (Code, Message); the factory writes Message to detail and Code to the code extension.
Service Pattern
- Application:
public interface I{Feature}Service+recordDTOs (Input/Output) - Infrastructure:
internal class {Feature}Service(deps...) : I{Feature}Service- primary ctor,IOptions<T>for config - DI extension: C# 13
extension(IServiceCollection)syntax, called fromProgram.cs
DTO Naming
| Layer | Pattern | Example |
| WebApi Request | {Operation}Request | LoginRequest |
| WebApi Response | {Entity}Response | UserResponse |
| Application Input | {Operation}Input | RegisterInput |
| Application Output | {Entity}Output | UserOutput |
Mappers: internal static class {Feature}Mapper with extension methods (request.ToInput(), output.ToResponse()).
WebApi response DTOs: classes with init properties and [UsedImplicitly] from JetBrains.Annotations.
Controller Conventions
- Authenticated endpoints extend
ApiController([Authorize], routeapi/v1/[controller]) - Public endpoints use
ControllerBasedirectly (routeapi/[controller]) - Include
/// <summary>,[ProducesResponseType]per status code, andCancellationTokenas last param - Never
/// <param name="cancellationToken">- it leaks into OASrequestBody.description - File uploads:
[FromForm]withIFormFile,[Consumes("multipart/form-data")],[RequestSizeLimit(bytes)] - Error responses: use
ProblemFactory.Create()- avoidNotFound(),BadRequest(), or anonymous objects - Success responses:
Ok(response),Created(string.Empty, response) [ProducesResponseType]withouttypeof(...)on error codes (400, 401, 403, 404, 429)
Validation
FluentValidation auto-discovered from WebApi assembly. Co-locate validators with request DTOs.
| Rule Type | Convention |
| New passwords | MinimumLength(6) + lowercase + uppercase + digit rules |
| Existing passwords | NotEmpty() + MaximumLength(255) only |
| Optional fields | .When(x => !string.IsNullOrEmpty(x.Field)) |
| URL fields | Uri.TryCreate + restrict to http/https schemes |
| Shared patterns | Extract to ValidationConstants.cs |
Error Messages and Codes
- Client-facing errors are centralized as
static readonly Errorentries inErrorMessages.csnested classes.Error(Code, Message)pairs a stable, machine-readable snake_case code with the human-readable message. - Codes are derived from the declaring location:
{nested_class}_{field_name}in snake_case (ErrorMessages.ExternalAuth.StateExpired->external_auth_state_expired).ErrorMessagesTestsenforces the pattern and global uniqueness. - Every
ProblemDetailsresponse carries the code in thecodeextension (ProblemFactoryfor controllers/middleware,ProblemFactory.EnsureCodeinAddProblemDetailsfor framework-generated bodies:validation_failedfor model validation, snake_case reason phrase such asnot_foundotherwise).ProblemDetailsSchemaTransformerdocuments it in OpenAPI. - Codes are a public contract: adding one is additive, renaming or removing one is a breaking change for API consumers (frontend maps on codes, not on
detailtext). - Runtime values (role names, user IDs, framework errors): log server-side via
ILogger, never inResult.Failure() - Identity errors: log the error
.Codevalues server-side, return a staticErrorMessagesentry to the client - never echo.Description(it can embed emails and enables account enumeration). No exceptions: register, change-password, and reset-password all return static messages; client-side validators provide the detailed password-policy feedback. - To add: create
public static readonly Error X = new("{class}_{x}", "...")in the matchingErrorMessages.csnested class. Dynamic values go in logs, not in Result.
Authorization
Permission System
Atomic permissions via [RequirePermission("permission.name")] on controller actions. Permissions stored as role claims, embedded in JWT as "permission" claims.
AppPermissions.cs: constants discovered via reflection (AppPermissions.All)PermissionAuthorizationHandler: Superuser bypass -> claim match -> deny- Never class-level
[Authorize(Roles)]on controllers using permissions - To add a role: add
public const stringtoAppRoles.cs- reflection discovers it, seeding picks it up automatically.
Role Hierarchy
Superuser (3) > Admin (2) > User (1) > Custom (0). Enforced by Admin service:
- Cannot manage users at/above your rank
- Cannot assign/remove roles at/above your rank
- Cannot modify your own roles, lock yourself, or delete yourself
Permission changes on a role -> invalidate refresh tokens + rotate security stamps + clear cache for all affected users.
Repository Pattern
IBaseEntityRepository<T> provides CRUD with automatic soft-delete filtering (global query filter). Open generic registration covers basic entities.
Custom repositories: extend IBaseEntityRepository<T> in Application, implement in Infrastructure with BaseEntityRepository<T>. Avoid exposing IQueryable across layer boundaries.
Pagination: Paginate(int pageNumber, int pageSize) extension on IQueryable<T> returns IQueryable<T> (applies Skip/Take).
Caching
HybridCache (.NET built-in) provides L1 in-process caching with stampede protection. Keys defined in CacheKeys constants. UserCacheInvalidationInterceptor auto-clears user cache on entity changes.
File Storage
IFileStorageService - generic S3-compatible interface (Upload, Download, Delete, Exists). Implementation: S3FileStorageService (works with MinIO locally, any S3-compatible provider in production).
Uploading files from a controller:
- Accept
IFormFilevia[FromForm]+[Consumes("multipart/form-data")]+[RequestSizeLimit] - Read to
byte[]in the controller:using var ms = new MemoryStream(); await file.CopyToAsync(ms); var data = ms.ToArray(); - Pass to the service for validation/processing
- Store via
fileStorageService.UploadAsync(key, data, contentType, ct)- returnsResult
Storage keys: Use {feature}/{id}.{ext} pattern (e.g., avatars/{userId}.webp).
Email Templates
Fluid (Liquid) templates with 3-file pattern ({name}.liquid, {name}.subject.liquid, {name}.text.liquid). See the /add-email-template skill for full template patterns and model records.
OpenAPI
/// <summary>on every controller action and DTO property -> generates OAS descriptions[ProducesResponseType]declares all possible status codes per actionEnumSchemaTransformerauto-documents enum values- Scalar UI at
/scalar/v1(development only)
Options Pattern
public sealed class {Name}Options
{
public const string SectionName = "{Section}";
/// <summary>Gets or sets the ...</summary>
[Required]
public string Value { get; init; } = string.Empty;
}Register with BindConfiguration, ValidateDataAnnotations, ValidateOnStart.
Testing
| Project | Tests | Dependencies |
Unit.Tests | Pure logic (Shared, Domain, Application) | None - no mocks, no DI |
Component.Tests | Service business logic | TestDbContextFactory (InMemory), NSubstitute, IdentityMockHelpers |
Api.Tests | Full HTTP pipeline (routes, auth, status codes) | CustomWebApplicationFactory, TestAuthHandler |
Architecture.Tests | Layer deps, naming, visibility | NetArchTest |
API test auth: "Authorization", "Test" (basic user), TestAuth.WithPermissions(...) (specific perms), TestAuth.Superuser().
Response contracts: frozen records in Contracts/ResponseContracts.cs - deserialize and assert key fields.
Aspire (Local Development)
Run: dotnet run --project src/backend/MyProject.AppHost - launches PostgreSQL, MinIO, MailPit, API, and Frontend. See /add-aspire-dep skill for adding dependencies.
Logging gotcha: Serilog bridges to OTEL via writeToProviders: true - do NOT add Serilog.Sinks.OpenTelemetry (causes duplicate logs).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

