claude-code-arcane 1.2.0 → 1.4.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 +16 -0
- package/README.md +5 -2
- package/agents/ecommerce/ecommerce-architect.md +60 -0
- package/agents/engineering/dotnet-engineer.md +78 -0
- package/docs/SKILLS-CATALOG.md +45 -3
- package/docs/USER-GUIDE.md +2 -2
- package/package.json +1 -1
- package/profiles/backend-dotnet.yaml +54 -0
- package/profiles/ecommerce.yaml +48 -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/cart-checkout/SKILL.md +85 -0
- package/skills/cart-checkout/references/checkout-flow-spec.md +50 -0
- package/skills/cold-outreach/SKILL.md +65 -0
- package/skills/cold-outreach/references/recruiter-playbook.md +65 -0
- package/skills/commerce-analytics/SKILL.md +85 -0
- package/skills/commerce-analytics/references/ga4-ecommerce-events.md +63 -0
- package/skills/commerce-analytics/scripts/ga4_ecommerce_validate.py +134 -0
- package/skills/commerce-data-model/SKILL.md +84 -0
- package/skills/commerce-data-model/references/data-model-spec.md +79 -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/inventory-stock/SKILL.md +86 -0
- package/skills/inventory-stock/scripts/restock_forecast.py +173 -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/merchant-feed/SKILL.md +85 -0
- package/skills/merchant-feed/references/feed-attribute-map.md +47 -0
- package/skills/merchant-feed/scripts/feed_validate.py +140 -0
- package/skills/network-map/SKILL.md +61 -0
- package/skills/network-map/scripts/network_map.py +109 -0
- package/skills/order-lifecycle/SKILL.md +86 -0
- package/skills/order-lifecycle/references/order-state-machine.md +53 -0
- package/skills/payments-architecture/SKILL.md +80 -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
- package/skills/product-listing/SKILL.md +84 -0
- package/skills/promotions-discounts/SKILL.md +80 -0
- package/skills/shipping-tax/SKILL.md +85 -0
- package/skills/storefront-architecture/SKILL.md +80 -0
- package/skills/subscription-billing/SKILL.md +81 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Patrones Idiomáticos (.NET 10 / ASP.NET Core)
|
|
2
|
+
|
|
3
|
+
## Handler por caso de uso
|
|
4
|
+
|
|
5
|
+
Un request (record) + un handler. Con MediatR o con un sender hand-rolled mínimo.
|
|
6
|
+
|
|
7
|
+
```csharp
|
|
8
|
+
public sealed record CreateOrder(Guid CustomerId, decimal Amount) : IRequest<Result<Guid>>;
|
|
9
|
+
|
|
10
|
+
internal sealed class CreateOrderHandler(IApplicationDbContext db)
|
|
11
|
+
: IRequestHandler<CreateOrder, Result<Guid>>
|
|
12
|
+
{
|
|
13
|
+
public async Task<Result<Guid>> Handle(CreateOrder cmd, CancellationToken ct)
|
|
14
|
+
{
|
|
15
|
+
var order = Order.Create(cmd.CustomerId, cmd.Amount);
|
|
16
|
+
db.Orders.Add(order);
|
|
17
|
+
await db.SaveChangesAsync(ct);
|
|
18
|
+
return order.Id;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## IApplicationDbContext (EF Core detrás de una interfaz, sin repo genérico)
|
|
24
|
+
|
|
25
|
+
Exponé el `DbContext` a través de una interfaz en `Application`; implementala en
|
|
26
|
+
`Infrastructure`. El `DbContext` ya es Unit of Work + repositorio: no lo envuelvas.
|
|
27
|
+
|
|
28
|
+
```csharp
|
|
29
|
+
// Application/Common/IApplicationDbContext.cs
|
|
30
|
+
public interface IApplicationDbContext
|
|
31
|
+
{
|
|
32
|
+
DbSet<Order> Orders { get; }
|
|
33
|
+
DbSet<Customer> Customers { get; }
|
|
34
|
+
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Infrastructure/Persistence/AppDbContext.cs
|
|
38
|
+
public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
|
|
39
|
+
: DbContext(options), IApplicationDbContext
|
|
40
|
+
{
|
|
41
|
+
public DbSet<Order> Orders => Set<Order>();
|
|
42
|
+
public DbSet<Customer> Customers => Set<Customer>();
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Pipeline behaviors (validación, logging, transacción)
|
|
47
|
+
|
|
48
|
+
Cross-cutting concerns alrededor de cada handler, sin ensuciar la lógica.
|
|
49
|
+
|
|
50
|
+
```csharp
|
|
51
|
+
public sealed class ValidationBehavior<TReq, TRes>(IEnumerable<IValidator<TReq>> validators)
|
|
52
|
+
: IPipelineBehavior<TReq, TRes> where TReq : notnull
|
|
53
|
+
{
|
|
54
|
+
public async Task<TRes> Handle(TReq req, RequestHandlerDelegate<TRes> next, CancellationToken ct)
|
|
55
|
+
{
|
|
56
|
+
foreach (var v in validators)
|
|
57
|
+
{
|
|
58
|
+
var result = await v.ValidateAsync(req, ct);
|
|
59
|
+
if (!result.IsValid) throw new ValidationException(result.Errors);
|
|
60
|
+
}
|
|
61
|
+
return await next();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Un `TransactionBehavior` similar abre `BeginTransactionAsync` antes de `next()` y
|
|
67
|
+
hace commit/rollback alrededor: el UnitOfWork vive acá, no en repos a mano.
|
|
68
|
+
|
|
69
|
+
## Result pattern para fallos esperados
|
|
70
|
+
|
|
71
|
+
No tires excepciones para flujo de negocio esperado (not found, validación, conflicto).
|
|
72
|
+
Devolvé `Result<T>` (o `ErrorOr<T>`) y mapealo a HTTP en el endpoint.
|
|
73
|
+
|
|
74
|
+
```csharp
|
|
75
|
+
public readonly record struct Error(string Code, string Message);
|
|
76
|
+
|
|
77
|
+
public async Task<Result<OrderDto>> Handle(GetOrder q, CancellationToken ct)
|
|
78
|
+
{
|
|
79
|
+
var order = await db.Orders.FindAsync([q.Id], ct);
|
|
80
|
+
return order is null
|
|
81
|
+
? new Error("Order.NotFound", "Order no existe")
|
|
82
|
+
: order.ToDto();
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Outbox pattern (consistencia DB + eventos)
|
|
87
|
+
|
|
88
|
+
Para publicar eventos de forma confiable: persistí el evento en una tabla `Outbox`
|
|
89
|
+
**dentro de la misma transacción** que el cambio de datos. Un background worker
|
|
90
|
+
(`BackgroundService`) lee la tabla y publica al broker; así nunca queda el estado
|
|
91
|
+
guardado sin el evento ni el evento sin el estado.
|
|
92
|
+
|
|
93
|
+
## TypedResults en minimal API endpoints
|
|
94
|
+
|
|
95
|
+
El endpoint es delgado: traduce HTTP ↔ request y mapea el `Result` a status code.
|
|
96
|
+
|
|
97
|
+
```csharp
|
|
98
|
+
public static class OrderEndpoints
|
|
99
|
+
{
|
|
100
|
+
public static void Map(this IEndpointRouteBuilder app)
|
|
101
|
+
{
|
|
102
|
+
var group = app.MapGroup("/orders").WithTags("Orders");
|
|
103
|
+
|
|
104
|
+
group.MapPost("/", async (CreateOrder cmd, ISender sender, CancellationToken ct) =>
|
|
105
|
+
{
|
|
106
|
+
var result = await sender.Send(cmd, ct);
|
|
107
|
+
return result.IsError
|
|
108
|
+
? TypedResults.Problem(result.FirstError.Message)
|
|
109
|
+
: TypedResults.Created($"/orders/{result.Value}", result.Value);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`TypedResults` da el tipo de respuesta exacto (útil para OpenAPI y tests) en vez del
|
|
116
|
+
`Results` no tipado.
|
|
117
|
+
|
|
118
|
+
_Ref: https://www.milanjovanovic.tech/blog/internal-vs-public-apis-in-clean-architecture · https://github.com/ardalis/CleanArchitecture · https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses_
|
|
@@ -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_
|