claude-code-arcane 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +2 -0
- package/agents/engineering/dotnet-engineer.md +78 -0
- package/docs/SKILLS-CATALOG.md +26 -3
- package/package.json +1 -1
- package/profiles/backend-dotnet.yaml +54 -0
- package/profiles/job-hunt.yaml +35 -0
- package/profiles/unity-design.yaml +1 -0
- package/profiles/unity-dev.yaml +1 -0
- package/rules/dotnet-code.md +64 -0
- package/skills/cold-outreach/SKILL.md +65 -0
- package/skills/cold-outreach/references/recruiter-playbook.md +65 -0
- package/skills/cover-letter/SKILL.md +66 -0
- package/skills/cv-ats-export/SKILL.md +64 -0
- package/skills/cv-ats-export/scripts/cv_export.py +306 -0
- package/skills/cv-tailor/SKILL.md +70 -0
- package/skills/cv-tailor/references/ats-keywords.md +46 -0
- package/skills/dotnet-architecture/SKILL.md +66 -0
- package/skills/dotnet-architecture/references/anti-patterns.md +12 -0
- package/skills/dotnet-architecture/references/checklist.md +19 -0
- package/skills/dotnet-architecture/references/patterns.md +118 -0
- package/skills/dotnet-architecture/references/project-structure.md +78 -0
- package/skills/dotnet-best-practices/SKILL.md +76 -0
- package/skills/dotnet-best-practices/references/api-design.md +75 -0
- package/skills/dotnet-best-practices/references/architecture.md +62 -0
- package/skills/dotnet-best-practices/references/async.md +62 -0
- package/skills/dotnet-best-practices/references/database.md +69 -0
- package/skills/dotnet-best-practices/references/dependency-injection.md +73 -0
- package/skills/dotnet-best-practices/references/devops.md +76 -0
- package/skills/dotnet-best-practices/references/error-handling.md +72 -0
- package/skills/dotnet-best-practices/references/performance.md +63 -0
- package/skills/dotnet-best-practices/references/security.md +73 -0
- package/skills/dotnet-best-practices/references/testing.md +76 -0
- package/skills/dotnet-scaffold/SKILL.md +99 -0
- package/skills/install-mcp/SKILL.md +107 -0
- package/skills/install-mcp/references/manual-setup.md +92 -0
- package/skills/interview-prep/SKILL.md +69 -0
- package/skills/interview-prep/references/star-framework.md +42 -0
- package/skills/job-hunt/SKILL.md +92 -0
- package/skills/job-hunt/references/templates/Aplicacion.md +48 -0
- package/skills/job-hunt/references/templates/CV Custom.md +53 -0
- package/skills/job-hunt/references/templates/Contacto.md +30 -0
- package/skills/job-hunt/references/templates/Dashboard.md +45 -0
- package/skills/job-hunt/references/templates/Empresa.md +36 -0
- package/skills/job-hunt/references/templates/Entrevista.md +44 -0
- package/skills/job-hunt/references/templates/Perfil.md +38 -0
- package/skills/job-search/SKILL.md +83 -0
- package/skills/job-search/references/scoring-rubric.md +43 -0
- package/skills/linkedin-optimize/SKILL.md +79 -0
- package/skills/master-profile/SKILL.md +69 -0
- package/skills/network-map/SKILL.md +61 -0
- package/skills/network-map/scripts/network_map.py +109 -0
- package/skills/personal-brand/SKILL.md +54 -0
- package/skills/personal-brand/references/post-pillars.md +66 -0
- package/skills/portfolio-site/SKILL.md +59 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Estructura de Directorios
|
|
2
|
+
|
|
3
|
+
Dos formas válidas de estructurar un backend .NET 10 / ASP.NET Core. Elegí según
|
|
4
|
+
complejidad del dominio (ver `when-to-use` en la SKILL).
|
|
5
|
+
|
|
6
|
+
## Vertical Slice Architecture (un proyecto)
|
|
7
|
+
|
|
8
|
+
Cada feature es un folder autocontenido: request, handler, validator y endpoint
|
|
9
|
+
viven juntos. Tocás un caso de uso → tocás un solo archivo/carpeta.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
Features/
|
|
14
|
+
Orders/
|
|
15
|
+
CreateOrder.cs ← record Command + Handler + Validator juntos
|
|
16
|
+
GetOrders.cs ← record Query + Handler (paginación/proyección)
|
|
17
|
+
OrderEndpoints.cs ← MapGroup("/orders") + TypedResults
|
|
18
|
+
Customers/
|
|
19
|
+
RegisterCustomer.cs
|
|
20
|
+
CustomerEndpoints.cs
|
|
21
|
+
Common/
|
|
22
|
+
Behaviors/ ← ValidationBehavior, LoggingBehavior, TxBehavior
|
|
23
|
+
Result.cs ← Result<T> / Error compartido
|
|
24
|
+
Infrastructure/
|
|
25
|
+
AppDbContext.cs ← EF Core DbContext (DbSet<> por entidad)
|
|
26
|
+
Migrations/ ← migraciones versionadas en git
|
|
27
|
+
Program.cs ← composition root: DI, pipeline, MapEndpoints
|
|
28
|
+
appsettings.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Cada slice depende de `Common` e `Infrastructure`, no de otros slices.
|
|
32
|
+
- Un slice nuevo = un archivo nuevo; borrarlo no rompe a los demás (bajo acoplamiento).
|
|
33
|
+
- El `DbContext` es compartido; cada handler usa solo los `DbSet<>` que necesita.
|
|
34
|
+
|
|
35
|
+
## Clean Architecture (cuatro proyectos)
|
|
36
|
+
|
|
37
|
+
Separación por capa técnica. Las dependencias apuntan **hacia adentro**: el dominio
|
|
38
|
+
no conoce a nadie; la infraestructura y la API dependen de las capas internas.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
src/
|
|
42
|
+
MyApp.Domain/ ← Entidades, value objects, domain events, reglas
|
|
43
|
+
Entities/Order.cs ← cero deps externas (ni EF, ni ASP.NET)
|
|
44
|
+
ValueObjects/Money.cs
|
|
45
|
+
Common/Entity.cs
|
|
46
|
+
MyApp.Application/ ← Casos de uso (handlers), DTOs, interfaces
|
|
47
|
+
Orders/CreateOrder.cs
|
|
48
|
+
Common/IApplicationDbContext.cs ← expone DbSet<> + SaveChangesAsync
|
|
49
|
+
Common/Behaviors/
|
|
50
|
+
MyApp.Infrastructure/ ← EF Core, Identity, servicios externos
|
|
51
|
+
Persistence/AppDbContext.cs ← : DbContext, IApplicationDbContext
|
|
52
|
+
Persistence/Migrations/
|
|
53
|
+
Services/EmailSender.cs
|
|
54
|
+
DependencyInjection.cs
|
|
55
|
+
MyApp.Api/ ← Endpoints, Program.cs, wiring de DI
|
|
56
|
+
Endpoints/OrderEndpoints.cs
|
|
57
|
+
Program.cs
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Dirección de las referencias (`.csproj`)
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
MyApp.Api → referencia → MyApp.Infrastructure, MyApp.Application
|
|
64
|
+
MyApp.Infrastructure → referencia → MyApp.Application
|
|
65
|
+
MyApp.Application → referencia → MyApp.Domain
|
|
66
|
+
MyApp.Domain → referencia → (NADA)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `Domain.csproj` no tiene ningún `<ProjectReference>` ni paquete de framework.
|
|
70
|
+
- `Application` define `IApplicationDbContext`; `Infrastructure` lo implementa.
|
|
71
|
+
Así la lógica de negocio depende de una abstracción, no de EF Core directamente.
|
|
72
|
+
- La inversión de dependencias se logra en `Api/Program.cs`, que es el único lugar
|
|
73
|
+
que conoce a las cuatro capas y arma el grafo de DI.
|
|
74
|
+
|
|
75
|
+
> Regla de oro: si una flecha de dependencia apunta hacia afuera del dominio, la
|
|
76
|
+
> arquitectura está rota. El dominio es el centro estable.
|
|
77
|
+
|
|
78
|
+
_Ref: https://github.com/ardalis/CleanArchitecture · https://github.com/nadirbad/VerticalSliceArchitecture · https://www.milanjovanovic.tech/blog/vertical-slice-architecture-dotnet_
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dotnet-best-practices
|
|
3
|
+
description: "ASP.NET Core / C# production best practices: 40 reglas en 10 categorías (arquitectura, DI, errores, seguridad, performance, async, EF Core, testing, API, devops) priorizadas por impacto. Usar al escribir/revisar código .NET backend."
|
|
4
|
+
category: "backend"
|
|
5
|
+
argument-hint: "[architecture|di|errors|security|performance|async|database|testing|api|all]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Bash, Write, Edit, Task
|
|
8
|
+
---
|
|
9
|
+
# dotnet-best-practices — Guía priorizada por impacto
|
|
10
|
+
|
|
11
|
+
40 reglas para aplicaciones ASP.NET Core (.NET 10) production-ready, organizadas en 10 categorías y priorizadas de **CRITICAL** a **LOW-MEDIUM**. Cada regla del catálogo (en `references/`) trae impacto, ejemplo incorrecto, ejemplo correcto y link a docs.
|
|
12
|
+
|
|
13
|
+
## MANDATORY WORKFLOW
|
|
14
|
+
|
|
15
|
+
**No aplicar las 40 reglas a ciegas. Diagnosticar primero, priorizar por impacto.**
|
|
16
|
+
|
|
17
|
+
### Step 0: Determinar scope
|
|
18
|
+
|
|
19
|
+
1. **¿Código nuevo o auditoría de existente?** Si es auditoría, leer primero el feature/proyecto objetivo.
|
|
20
|
+
2. **¿Qué categoría aplica?** Mapear el área del problema (ver tabla) o usar `all` para review completa.
|
|
21
|
+
3. **¿Hay síntomas concretos?** (deadlocks, N+1, 500 inconsistentes, leaks) → ir directo a la categoría relevante.
|
|
22
|
+
|
|
23
|
+
### Step 1: Priorizar por impacto
|
|
24
|
+
|
|
25
|
+
Aplicar en este orden — no bajar de nivel hasta resolver el anterior:
|
|
26
|
+
|
|
27
|
+
| Prioridad | Categoría | Impacto | Reference |
|
|
28
|
+
|-----------|-----------|---------|-----------|
|
|
29
|
+
| 1 | Architecture | CRITICAL | `references/architecture.md` |
|
|
30
|
+
| 2 | Dependency Injection | CRITICAL | `references/dependency-injection.md` |
|
|
31
|
+
| 3 | Async & Concurrency | HIGH | `references/async.md` |
|
|
32
|
+
| 4 | Error Handling | HIGH | `references/error-handling.md` |
|
|
33
|
+
| 5 | Security | HIGH | `references/security.md` |
|
|
34
|
+
| 6 | Performance | HIGH | `references/performance.md` |
|
|
35
|
+
| 7 | Database & EF Core | MEDIUM-HIGH | `references/database.md` |
|
|
36
|
+
| 8 | Testing | MEDIUM-HIGH | `references/testing.md` |
|
|
37
|
+
| 9 | API Design | MEDIUM | `references/api-design.md` |
|
|
38
|
+
| 10 | DevOps & Deployment | LOW-MEDIUM | `references/devops.md` |
|
|
39
|
+
|
|
40
|
+
### Step 2: Aplicar + verificar
|
|
41
|
+
|
|
42
|
+
Para cada regla relevante: leer el ejemplo correcto en el reference, aplicarlo, y verificar contra el checklist de cierre.
|
|
43
|
+
|
|
44
|
+
### Step 3: Checklist de cierre
|
|
45
|
+
|
|
46
|
+
- [ ] `<Nullable>enable</Nullable>` + warnings as errors; cero `#nullable disable`
|
|
47
|
+
- [ ] Async end-to-end con `CancellationToken`; cero `.Result`/`.Wait()`/`.GetAwaiter().GetResult()`
|
|
48
|
+
- [ ] DI por constructor con scope correcto; sin `BuildServiceProvider()` manual ni service locator
|
|
49
|
+
- [ ] Errores vía `ProblemDetails` (RFC 7807); sin stack traces expuestos
|
|
50
|
+
- [ ] Sin N+1 en EF Core (`Include`/projection); `AsNoTracking()` en lecturas
|
|
51
|
+
- [ ] Migraciones versionadas; nunca `EnsureCreated()` en prod
|
|
52
|
+
- [ ] AuthZ por policies declarativas; secrets en secret manager (no en `appsettings.json`)
|
|
53
|
+
- [ ] Structured logging (Serilog JSON) + health checks
|
|
54
|
+
- [ ] Tests con xUnit; integración contra Postgres real (Testcontainers), no mocks de DbContext
|
|
55
|
+
|
|
56
|
+
Si todo el checklist pasa → código **COMPLIANT**. Antes de escribir cambios significativos, confirmar el approach con el usuario (Question → Decision → Approval).
|
|
57
|
+
|
|
58
|
+
## Tabla de mapeo síntoma → categoría
|
|
59
|
+
|
|
60
|
+
| Síntoma | Categoría |
|
|
61
|
+
|---------|-----------|
|
|
62
|
+
| Deadlocks / thread pool starvation / app cuelga | Async, Performance |
|
|
63
|
+
| Lentitud en endpoints, muchas queries | Performance, Database |
|
|
64
|
+
| `InvalidOperationException` de DI / scopes / captured dependency | DI, Architecture |
|
|
65
|
+
| Errores 500 inconsistentes / leaks de stack traces | Error Handling |
|
|
66
|
+
| Endpoints expuestos / brute force / data sensible en logs | Security |
|
|
67
|
+
| Tests frágiles o que pegan a servicios/DB reales sin control | Testing |
|
|
68
|
+
| Respuestas inconsistentes / over-fetching de entidades | API Design |
|
|
69
|
+
|
|
70
|
+
## Próximos pasos
|
|
71
|
+
|
|
72
|
+
Para estructurar el proyecto → `/dotnet-architecture`. Para iniciar un backend nuevo con estos defaults → `/dotnet-scaffold`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
_Adaptado de [github/awesome-copilot](https://github.com/github/awesome-copilot) (dotnet-best-practices), [dotnet/skills](https://github.com/dotnet/skills) y [Aaronontheweb/dotnet-skills](https://github.com/Aaronontheweb/dotnet-skills). Reorganizado al formato skill+references de Arcane._
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 9. API Design (MEDIUM)
|
|
2
|
+
|
|
3
|
+
## 9.1 DTOs, Never Expose EF Entities — MEDIUM
|
|
4
|
+
|
|
5
|
+
Serializar entidades de EF filtra columnas internas, arrastra relaciones y acopla el contrato HTTP al schema de la base. Usar DTOs/records dedicados.
|
|
6
|
+
|
|
7
|
+
**Incorrecto:**
|
|
8
|
+
```csharp
|
|
9
|
+
app.MapGet("/users/{id}", async (int id, AppDbContext db) => await db.Users.FindAsync(id)); // expone la entidad
|
|
10
|
+
```
|
|
11
|
+
**Correcto:**
|
|
12
|
+
```csharp
|
|
13
|
+
public record UserDto(int Id, string Email);
|
|
14
|
+
app.MapGet("/users/{id}", async (int id, AppDbContext db) =>
|
|
15
|
+
await db.Users.Where(u => u.Id == id).Select(u => new UserDto(u.Id, u.Email)).FirstOrDefaultAsync());
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 9.2 API Versioning — MEDIUM
|
|
19
|
+
|
|
20
|
+
Versionar la API permite evolucionar el contrato sin romper clientes existentes. `Asp.Versioning` lo integra con minimal APIs y OpenAPI.
|
|
21
|
+
|
|
22
|
+
**Correcto:**
|
|
23
|
+
```csharp
|
|
24
|
+
builder.Services.AddApiVersioning(o => o.DefaultApiVersion = new ApiVersion(1, 0));
|
|
25
|
+
var v1 = app.NewVersionedApi().MapGroup("/api/v{version:apiVersion}").HasApiVersion(1.0);
|
|
26
|
+
v1.MapGet("/orders", GetOrders);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 9.3 Consistent Errors with ProblemDetails — MEDIUM
|
|
30
|
+
|
|
31
|
+
`ProblemDetails` (RFC 9457) da un formato de error estándar y machine-readable en toda la API.
|
|
32
|
+
|
|
33
|
+
**Correcto:**
|
|
34
|
+
```csharp
|
|
35
|
+
builder.Services.AddProblemDetails();
|
|
36
|
+
app.UseExceptionHandler();
|
|
37
|
+
// en un handler:
|
|
38
|
+
return TypedResults.Problem(title: "Order not found", statusCode: StatusCodes.Status404NotFound);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 9.4 Correct Status Codes via TypedResults — MEDIUM
|
|
42
|
+
|
|
43
|
+
`TypedResults` devuelve el status code correcto de forma tipada y mejora los metadatos de OpenAPI.
|
|
44
|
+
|
|
45
|
+
**Correcto:**
|
|
46
|
+
```csharp
|
|
47
|
+
app.MapPost("/orders", async (CreateOrder cmd, IOrderService svc) =>
|
|
48
|
+
{
|
|
49
|
+
var order = await svc.CreateAsync(cmd);
|
|
50
|
+
return TypedResults.Created($"/orders/{order.Id}", order); // 201 + Location
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 9.5 Pagination & Idempotency for Writes — MEDIUM
|
|
55
|
+
|
|
56
|
+
Paginar colecciones evita respuestas ilimitadas; una Idempotency-Key hace que un POST repetido (reintento de red) no duplique el recurso.
|
|
57
|
+
|
|
58
|
+
**Correcto:**
|
|
59
|
+
```csharp
|
|
60
|
+
app.MapGet("/orders", (int page = 1, int pageSize = 20) => /* Skip/Take con límites */ );
|
|
61
|
+
app.MapPost("/payments", ([FromHeader(Name = "Idempotency-Key")] string key, Payment p) =>
|
|
62
|
+
/* deduplicar por key antes de procesar */ );
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 9.6 Built-in OpenAPI — LOW
|
|
66
|
+
|
|
67
|
+
`Microsoft.AspNetCore.OpenApi` genera el documento OpenAPI sin dependencias externas.
|
|
68
|
+
|
|
69
|
+
**Correcto:**
|
|
70
|
+
```csharp
|
|
71
|
+
builder.Services.AddOpenApi();
|
|
72
|
+
app.MapOpenApi(); // /openapi/v1.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
_Ref: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/apiversioning · https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors · https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview · https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses_
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 1. Architecture (CRITICAL)
|
|
2
|
+
|
|
3
|
+
## 1.1 Organize by Feature, Not Technical Layers — CRITICAL
|
|
4
|
+
|
|
5
|
+
Carpetas por feature (vertical slice) → cohesión alta y cambios localizados; las carpetas por capa técnica dispersan un cambio en 5 sitios.
|
|
6
|
+
|
|
7
|
+
**Incorrecto** — carpetas técnicas globales:
|
|
8
|
+
```csharp
|
|
9
|
+
// Controllers/, Services/, Repositories/, Dtos/ ... un cambio toca 4 carpetas
|
|
10
|
+
```
|
|
11
|
+
**Correcto** — un slice por feature:
|
|
12
|
+
```csharp
|
|
13
|
+
// Features/Orders/{ CreateOrder.cs, GetOrder.cs, OrdersEndpoints.cs, OrderDto.cs }
|
|
14
|
+
// Features/Users/{ RegisterUser.cs, UsersEndpoints.cs, UserDto.cs }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 1.2 Thin Endpoints — CRITICAL
|
|
18
|
+
|
|
19
|
+
El endpoint parsea, delega y forma la respuesta. Sin lógica de negocio: así es testeable y reutilizable desde otros transportes.
|
|
20
|
+
|
|
21
|
+
**Incorrecto:**
|
|
22
|
+
```csharp
|
|
23
|
+
app.MapPost("/orders", async (CreateOrderDto dto, AppDbContext db) =>
|
|
24
|
+
{
|
|
25
|
+
if (dto.Items.Count == 0) return Results.BadRequest(); // negocio en el endpoint
|
|
26
|
+
var total = dto.Items.Sum(i => i.Price * i.Qty); // negocio en el endpoint
|
|
27
|
+
db.Orders.Add(new Order { Total = total });
|
|
28
|
+
await db.SaveChangesAsync();
|
|
29
|
+
return Results.Ok();
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
**Correcto:**
|
|
33
|
+
```csharp
|
|
34
|
+
app.MapPost("/orders", async (CreateOrderRequest req, CreateOrderHandler handler, CancellationToken ct) =>
|
|
35
|
+
{
|
|
36
|
+
var result = await handler.HandleAsync(req, ct);
|
|
37
|
+
return result.IsSuccess ? Results.Created($"/orders/{result.Value.Id}", result.Value) : result.ToProblem();
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 1.3 Dependencies Point Inward / Slices Independent — HIGH
|
|
42
|
+
|
|
43
|
+
El dominio no conoce infraestructura (Clean Architecture): el handler depende de una abstracción `IOrderRepository`, no de `AppDbContext`. Entre slices, sin acoplamiento directo; coordinar vía eventos de dominio o un mediador.
|
|
44
|
+
|
|
45
|
+
## 1.4 Single Responsibility per Handler — HIGH
|
|
46
|
+
|
|
47
|
+
Un handler resuelve un caso de uso. `CreateOrderHandler` no envía emails ni cobra: emite un evento y otro handler reacciona. Mejora testabilidad y limita el blast radius de cada cambio.
|
|
48
|
+
|
|
49
|
+
## 1.5 Program.cs as Composition Root Only — MEDIUM-HIGH
|
|
50
|
+
|
|
51
|
+
`Program.cs` solo registra servicios y arma el pipeline; nada de lógica de negocio. Mover el wiring a extensiones (`builder.Services.AddOrdersFeature()`) mantiene el root legible.
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
55
|
+
builder.Services.AddOrdersFeature().AddAuthFeature();
|
|
56
|
+
var app = builder.Build();
|
|
57
|
+
app.UseExceptionHandler().UseAuthorization();
|
|
58
|
+
app.MapOrdersEndpoints();
|
|
59
|
+
app.Run();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
_Ref: https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures · https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/min-api-filters_
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 3. Async & Concurrency (HIGH)
|
|
2
|
+
|
|
3
|
+
## 3.1 Async All the Way — CRITICAL
|
|
4
|
+
|
|
5
|
+
`.Result`, `.Wait()` y `.GetAwaiter().GetResult()` bloquean el hilo: deadlocks y thread-pool starvation bajo carga. Propagar `async`/`await` de punta a punta.
|
|
6
|
+
|
|
7
|
+
**Incorrecto:**
|
|
8
|
+
```csharp
|
|
9
|
+
public Order Get(int id) => repo.GetAsync(id).Result; // bloquea el thread-pool
|
|
10
|
+
```
|
|
11
|
+
**Correcto:**
|
|
12
|
+
```csharp
|
|
13
|
+
public async Task<Order> GetAsync(int id, CancellationToken ct) => await repo.GetAsync(id, ct);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 3.2 Accept and Propagate CancellationToken — HIGH
|
|
17
|
+
|
|
18
|
+
El token aborta el trabajo cuando el cliente corta la conexión, liberando hilos y conexiones de DB. Pasarlo a cada llamada async, hasta EF Core y `HttpClient`.
|
|
19
|
+
|
|
20
|
+
**Correcto:**
|
|
21
|
+
```csharp
|
|
22
|
+
app.MapGet("/orders/{id:int}", async (int id, AppDbContext db, CancellationToken ct) =>
|
|
23
|
+
await db.Orders.FirstOrDefaultAsync(o => o.Id == id, ct) is { } o ? Results.Ok(o) : Results.NotFound());
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 3.3 No async void — HIGH
|
|
27
|
+
|
|
28
|
+
`async void` no se puede esperar y sus excepciones tumban el proceso. Usar `async Task`. La única excepción son los event handlers de UI/eventos.
|
|
29
|
+
|
|
30
|
+
**Incorrecto:**
|
|
31
|
+
```csharp
|
|
32
|
+
public async void Process() => await DoWorkAsync(); // excepción => crash no observable
|
|
33
|
+
```
|
|
34
|
+
**Correcto:**
|
|
35
|
+
```csharp
|
|
36
|
+
public async Task ProcessAsync(CancellationToken ct) => await DoWorkAsync(ct);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 3.4 ConfigureAwait(false) in Libraries — MEDIUM-HIGH
|
|
40
|
+
|
|
41
|
+
En código de librería reutilizable, `ConfigureAwait(false)` evita volver al contexto capturado y previene deadlocks en hosts con `SynchronizationContext`. En apps ASP.NET Core (sin contexto) no hace falta.
|
|
42
|
+
|
|
43
|
+
```csharp
|
|
44
|
+
var data = await httpClient.GetStringAsync(url, ct).ConfigureAwait(false);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 3.5 Task.WhenAll for Parallel Independent Work — MEDIUM-HIGH
|
|
48
|
+
|
|
49
|
+
Operaciones independientes en paralelo en vez de secuencial. No compartir un `DbContext` entre tasks concurrentes (no es thread-safe): usar un scope/contexto por task.
|
|
50
|
+
|
|
51
|
+
```csharp
|
|
52
|
+
var (user, prefs) = (await Task.WhenAll(GetUserAsync(id, ct), GetPrefsAsync(id, ct))) switch
|
|
53
|
+
{
|
|
54
|
+
var r => (r[0], r[1])
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 3.6 No Fire-and-Forget Without Error Handling — HIGH
|
|
59
|
+
|
|
60
|
+
Un `Task` descartado traga excepciones y puede morir al reciclar el proceso. Para trabajo en background usar un `BackgroundService` / cola, con logging del error.
|
|
61
|
+
|
|
62
|
+
_Ref: https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-scenarios · https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern_
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 7. Database & EF Core (MEDIUM-HIGH)
|
|
2
|
+
|
|
3
|
+
## 7.1 Avoid N+1 with Include or Projection — HIGH
|
|
4
|
+
|
|
5
|
+
Acceder a relaciones en un loop dispara una query por fila. Usar `Include` o proyectar a un DTO con `Select` resuelve todo en una sola query.
|
|
6
|
+
|
|
7
|
+
**Incorrecto:**
|
|
8
|
+
```csharp
|
|
9
|
+
var orders = await db.Orders.ToListAsync();
|
|
10
|
+
foreach (var o in orders) o.Customer = await db.Customers.FindAsync(o.CustomerId); // N+1
|
|
11
|
+
```
|
|
12
|
+
**Correcto:**
|
|
13
|
+
```csharp
|
|
14
|
+
var orders = await db.Orders
|
|
15
|
+
.Select(o => new OrderDto(o.Id, o.Customer.Name, o.Total))
|
|
16
|
+
.ToListAsync();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 7.2 AsNoTracking for Read-Only Queries — HIGH
|
|
20
|
+
|
|
21
|
+
El change tracker añade overhead innecesario en lecturas. `AsNoTracking()` evita las snapshots y reduce allocations.
|
|
22
|
+
|
|
23
|
+
**Correcto:**
|
|
24
|
+
```csharp
|
|
25
|
+
var products = await db.Products.AsNoTracking().Where(p => p.Active).ToListAsync();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 7.3 DbContext is the Unit of Work — MEDIUM-HIGH
|
|
29
|
+
|
|
30
|
+
`DbContext` ya implementa Unit of Work + Repository. Envolverlo en un repositorio genérico oculta `Include`, proyecciones y `SaveChanges`, añadiendo abstracción sin valor.
|
|
31
|
+
|
|
32
|
+
**Incorrecto:**
|
|
33
|
+
```csharp
|
|
34
|
+
public class GenericRepository<T> { Task<T> GetById(int id); /* envuelve DbSet sin necesidad */ }
|
|
35
|
+
```
|
|
36
|
+
**Correcto:**
|
|
37
|
+
```csharp
|
|
38
|
+
// Inyectar AppDbContext (o un servicio de dominio que lo use) directamente
|
|
39
|
+
public sealed class OrderService(AppDbContext db) { /* usa db.Orders, db.SaveChangesAsync */ }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 7.4 Versioned Migrations, Never EnsureCreated in Prod — MEDIUM-HIGH
|
|
43
|
+
|
|
44
|
+
`EnsureCreated()` no usa migraciones y no es upgradeable: hay que migrar el schema versionado con la CLI de EF.
|
|
45
|
+
|
|
46
|
+
**Incorrecto:**
|
|
47
|
+
```csharp
|
|
48
|
+
await db.Database.EnsureCreatedAsync(); // no migrable, pierde historial de schema
|
|
49
|
+
```
|
|
50
|
+
**Correcto:**
|
|
51
|
+
```bash
|
|
52
|
+
dotnet ef migrations add AddOrderIndex
|
|
53
|
+
dotnet ef database update # en deploy: db.Database.MigrateAsync()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 7.5 Connection Resiliency & Split Queries — MEDIUM
|
|
57
|
+
|
|
58
|
+
Habilitar reintentos ante fallos transitorios y `AsSplitQuery()` para `Include` grandes evita la explosión cartesiana de un único JOIN.
|
|
59
|
+
|
|
60
|
+
**Correcto:**
|
|
61
|
+
```csharp
|
|
62
|
+
builder.Services.AddDbContext<AppDbContext>(o =>
|
|
63
|
+
o.UseNpgsql(conn, npgsql => npgsql.EnableRetryOnFailure()));
|
|
64
|
+
|
|
65
|
+
var blogs = await db.Blogs.Include(b => b.Posts).Include(b => b.Tags)
|
|
66
|
+
.AsSplitQuery().ToListAsync();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
_Ref: https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager-loading · https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations · https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency_
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# 2. Dependency Injection (CRITICAL)
|
|
2
|
+
|
|
3
|
+
## 2.1 Prefer Constructor Injection (Primary Constructors) — CRITICAL
|
|
4
|
+
|
|
5
|
+
Dependencias explícitas, type-safe y testeables. Con primary constructors (C# 12+) el boilerplate desaparece.
|
|
6
|
+
|
|
7
|
+
**Correcto:**
|
|
8
|
+
```csharp
|
|
9
|
+
public sealed class CreateOrderHandler(IOrderRepository repo, ILogger<CreateOrderHandler> logger)
|
|
10
|
+
{
|
|
11
|
+
public async Task HandleAsync(CreateOrderRequest req, CancellationToken ct)
|
|
12
|
+
{
|
|
13
|
+
logger.LogInformation("Creating order for {Customer}", req.CustomerId);
|
|
14
|
+
await repo.AddAsync(req.ToOrder(), ct);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 2.2 Use Correct Lifetimes — CRITICAL
|
|
20
|
+
|
|
21
|
+
`Scoped` para `DbContext` y handlers (una instancia por request). `Singleton` solo para servicios stateless y thread-safe. `Transient` para servicios livianos sin estado.
|
|
22
|
+
|
|
23
|
+
```csharp
|
|
24
|
+
builder.Services.AddDbContext<AppDbContext>(o => o.UseNpgsql(cs)); // Scoped por defecto
|
|
25
|
+
builder.Services.AddScoped<CreateOrderHandler>();
|
|
26
|
+
builder.Services.AddSingleton<IClock, SystemClock>(); // stateless
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 2.3 Never Inject Scoped into Singleton — CRITICAL
|
|
30
|
+
|
|
31
|
+
Es la "captured dependency": el Singleton retiene una instancia Scoped muerta tras el primer request → datos corruptos o `ObjectDisposedException`. Resolverlo bajo demanda con un scope.
|
|
32
|
+
|
|
33
|
+
**Incorrecto:**
|
|
34
|
+
```csharp
|
|
35
|
+
public sealed class CacheWarmer(AppDbContext db) : IHostedService { } // Singleton captura DbContext Scoped
|
|
36
|
+
```
|
|
37
|
+
**Correcto:**
|
|
38
|
+
```csharp
|
|
39
|
+
public sealed class CacheWarmer(IServiceScopeFactory scopeFactory) : IHostedService
|
|
40
|
+
{
|
|
41
|
+
public async Task StartAsync(CancellationToken ct)
|
|
42
|
+
{
|
|
43
|
+
await using var scope = scopeFactory.CreateAsyncScope();
|
|
44
|
+
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 2.4 No Service Locator / No Manual BuildServiceProvider() — HIGH
|
|
50
|
+
|
|
51
|
+
`provider.GetService<T>()` dentro de la lógica oculta dependencias y rompe los tests. `BuildServiceProvider()` en `Program.cs` crea un container paralelo (singletons duplicados, fugas). Inyectar por constructor (2.1).
|
|
52
|
+
|
|
53
|
+
## 2.5 Register by Interface — HIGH
|
|
54
|
+
|
|
55
|
+
Registrar contra la abstracción permite sustituir implementaciones (tests, features) sin tocar consumidores.
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 2.6 IOptions<T> Pattern for Config — MEDIUM-HIGH
|
|
62
|
+
|
|
63
|
+
Bind tipado y validado de configuración, en vez de leer `IConfiguration` con strings por todo el código.
|
|
64
|
+
|
|
65
|
+
```csharp
|
|
66
|
+
builder.Services.AddOptions<JwtOptions>()
|
|
67
|
+
.Bind(builder.Configuration.GetSection("Jwt"))
|
|
68
|
+
.ValidateDataAnnotations()
|
|
69
|
+
.ValidateOnStart();
|
|
70
|
+
// constructor(IOptions<JwtOptions> options) => _opts = options.Value;
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
_Ref: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection · https://learn.microsoft.com/en-us/dotnet/core/extensions/options_
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 10. DevOps & Deployment (LOW-MEDIUM)
|
|
2
|
+
|
|
3
|
+
## 10.1 Multi-Stage Dockerfile, Non-Root User — MEDIUM
|
|
4
|
+
|
|
5
|
+
Compilar con la imagen SDK y correr sobre la imagen `aspnet` runtime reduce el tamaño y la superficie de ataque. Usar un usuario no-root limita el blast radius.
|
|
6
|
+
|
|
7
|
+
**Correcto:**
|
|
8
|
+
```dockerfile
|
|
9
|
+
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
10
|
+
WORKDIR /src
|
|
11
|
+
COPY . .
|
|
12
|
+
RUN dotnet publish -c Release -o /app
|
|
13
|
+
|
|
14
|
+
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
|
15
|
+
WORKDIR /app
|
|
16
|
+
COPY --from=build /app .
|
|
17
|
+
USER $APP_UID
|
|
18
|
+
ENTRYPOINT ["dotnet", "Api.dll"]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 10.2 Health Checks (Liveness/Readiness) — MEDIUM
|
|
22
|
+
|
|
23
|
+
Los orquestadores (Kubernetes) necesitan endpoints separados: liveness (¿está vivo?) y readiness (¿puede recibir tráfico?, p. ej. DB conectada).
|
|
24
|
+
|
|
25
|
+
**Correcto:**
|
|
26
|
+
```csharp
|
|
27
|
+
builder.Services.AddHealthChecks().AddNpgSql(conn, tags: ["ready"]);
|
|
28
|
+
app.MapHealthChecks("/health/live", new() { Predicate = _ => false });
|
|
29
|
+
app.MapHealthChecks("/health/ready", new() { Predicate = c => c.Tags.Contains("ready") });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 10.3 Structured Logging (Serilog JSON) — MEDIUM
|
|
33
|
+
|
|
34
|
+
Logs en JSON son parseables por agregadores (Loki, ELK). Serilog con sink de consola en formato compacto facilita la observabilidad.
|
|
35
|
+
|
|
36
|
+
**Correcto:**
|
|
37
|
+
```csharp
|
|
38
|
+
builder.Host.UseSerilog((ctx, cfg) =>
|
|
39
|
+
cfg.WriteTo.Console(new Serilog.Formatting.Compact.CompactJsonFormatter()));
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 10.4 OpenTelemetry for Traces & Metrics — MEDIUM
|
|
43
|
+
|
|
44
|
+
OpenTelemetry instrumenta traces y métricas de forma estándar y exportable a cualquier backend OTLP.
|
|
45
|
+
|
|
46
|
+
**Correcto:**
|
|
47
|
+
```csharp
|
|
48
|
+
builder.Services.AddOpenTelemetry()
|
|
49
|
+
.WithTracing(t => t.AddAspNetCoreInstrumentation().AddOtlpExporter())
|
|
50
|
+
.WithMetrics(m => m.AddAspNetCoreInstrumentation().AddOtlpExporter());
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 10.5 Config per Environment & Graceful Shutdown — LOW-MEDIUM
|
|
54
|
+
|
|
55
|
+
Nunca hardcodear secrets ni connection strings: usar `appsettings.{Environment}.json` y variables de entorno. Registrar limpieza ante el shutdown con `IHostApplicationLifetime`.
|
|
56
|
+
|
|
57
|
+
**Correcto:**
|
|
58
|
+
```csharp
|
|
59
|
+
var conn = builder.Configuration.GetConnectionString("Default"); // env / appsettings, no hardcoded
|
|
60
|
+
app.Lifetime.ApplicationStopping.Register(() => Log.Information("Draining connections..."));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 10.6 CI: build, test, format — LOW
|
|
64
|
+
|
|
65
|
+
El pipeline debe fallar ante código que no compila, tests rojos o formato inconsistente.
|
|
66
|
+
|
|
67
|
+
**Correcto:**
|
|
68
|
+
```yaml
|
|
69
|
+
steps:
|
|
70
|
+
- run: dotnet restore
|
|
71
|
+
- run: dotnet build --no-restore -c Release
|
|
72
|
+
- run: dotnet test --no-build -c Release
|
|
73
|
+
- run: dotnet format --verify-no-changes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
_Ref: https://learn.microsoft.com/en-us/dotnet/core/docker/build-container · https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks · https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel · https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host_
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 4. Error Handling (HIGH)
|
|
2
|
+
|
|
3
|
+
## 4.1 Global IExceptionHandler + ProblemDetails — CRITICAL
|
|
4
|
+
|
|
5
|
+
Respuestas de error consistentes y estandarizadas (RFC 7807) sin try/catch repetido en cada endpoint. Registrar `AddProblemDetails()` y un `IExceptionHandler`.
|
|
6
|
+
|
|
7
|
+
**Correcto:**
|
|
8
|
+
```csharp
|
|
9
|
+
public sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
|
|
10
|
+
{
|
|
11
|
+
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
|
|
12
|
+
{
|
|
13
|
+
logger.LogError(ex, "Unhandled exception on {Path}", ctx.Request.Path);
|
|
14
|
+
await Results.Problem(statusCode: StatusCodes.Status500InternalServerError, title: "An error occurred")
|
|
15
|
+
.ExecuteAsync(ctx);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Program.cs
|
|
20
|
+
builder.Services.AddProblemDetails();
|
|
21
|
+
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
|
|
22
|
+
app.UseExceptionHandler();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 4.2 Don't Leak Stack Traces — CRITICAL
|
|
26
|
+
|
|
27
|
+
En producción no exponer `ex.Message`, stack traces ni detalles internos: filtran rutas, versiones y vectores de ataque. Loggear el detalle, devolver un mensaje genérico (ver 4.1).
|
|
28
|
+
|
|
29
|
+
## 4.3 Result Pattern for Expected Failures — HIGH
|
|
30
|
+
|
|
31
|
+
Fallos esperados (validación, "not found", reglas de negocio) no son excepciones: las excepciones son caras y para lo excepcional. Devolver un `Result`.
|
|
32
|
+
|
|
33
|
+
**Incorrecto:**
|
|
34
|
+
```csharp
|
|
35
|
+
if (order is null) throw new NotFoundException(); // control de flujo con excepciones
|
|
36
|
+
```
|
|
37
|
+
**Correcto:**
|
|
38
|
+
```csharp
|
|
39
|
+
public readonly record struct Result<T>(bool IsSuccess, T? Value, string? Error)
|
|
40
|
+
{
|
|
41
|
+
public static Result<T> Ok(T value) => new(true, value, null);
|
|
42
|
+
public static Result<T> Fail(string error) => new(false, default, error);
|
|
43
|
+
}
|
|
44
|
+
return order is null ? Result<Order>.Fail("Order not found") : Result<Order>.Ok(order);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 4.4 Validation → ValidationProblemDetails — HIGH
|
|
48
|
+
|
|
49
|
+
Los errores de validación se devuelven como `ValidationProblemDetails` (400) con el mapa campo→errores, no como 500.
|
|
50
|
+
|
|
51
|
+
```csharp
|
|
52
|
+
return Results.ValidationProblem(new Dictionary<string, string[]>
|
|
53
|
+
{
|
|
54
|
+
["email"] = ["Email is required"]
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 4.5 No Empty Catch; Log With Context — MEDIUM-HIGH
|
|
59
|
+
|
|
60
|
+
`catch {}` oculta fallos y vuelve la app indebuggeable. Capturar tipos específicos, loggear con structured logging y re-lanzar o devolver un `Result`.
|
|
61
|
+
|
|
62
|
+
**Incorrecto:**
|
|
63
|
+
```csharp
|
|
64
|
+
try { await Pay(ct); } catch { } // se traga el error
|
|
65
|
+
```
|
|
66
|
+
**Correcto:**
|
|
67
|
+
```csharp
|
|
68
|
+
try { await Pay(ct); }
|
|
69
|
+
catch (PaymentException ex) { logger.LogError(ex, "Payment failed for {OrderId}", orderId); throw; }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
_Ref: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling · https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors_
|