claude-code-arcane 1.1.1 → 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 +16 -0
- package/README.md +2 -0
- package/agents/engineering/dotnet-engineer.md +78 -0
- package/dist/cli.js +284 -72
- package/docs/RELEASE-SETUP.md +99 -0
- package/docs/SKILLS-CATALOG.md +26 -3
- package/docs/presentations/arcane-overview-12.pptx +0 -0
- package/docs/presentations/arcane-overview.pptx +0 -0
- package/docs/presentations/build_arcane_deck.py +310 -0
- package/docs/presentations/build_arcane_deck_12.py +399 -0
- 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,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dotnet-architecture
|
|
3
|
+
description: "Arquitectura de backends .NET: Vertical Slice Architecture (features) y Clean Architecture (capas), con guía de cuándo usar cada una, estructura, patrones (MediatR/Result/outbox) y anti-patterns. Usar al diseñar o revisar la estructura de un proyecto ASP.NET Core."
|
|
4
|
+
category: "backend"
|
|
5
|
+
argument-hint: "[vertical-slice|clean|when-to-use]"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
allowed-tools: Read, Glob, Grep, Bash, Write, Edit, Task
|
|
8
|
+
---
|
|
9
|
+
# dotnet-architecture — VSA & Clean Architecture para .NET
|
|
10
|
+
|
|
11
|
+
Dos enfoques production-ready para estructurar un backend ASP.NET Core. No son excluyentes: muchos proyectos
|
|
12
|
+
usan Clean Architecture en el borde y slices verticales adentro. Esta skill ayuda a **elegir** y a aplicar
|
|
13
|
+
cada uno correctamente.
|
|
14
|
+
|
|
15
|
+
## MANDATORY WORKFLOW
|
|
16
|
+
|
|
17
|
+
**No imponer arquitectura por moda. Elegir según complejidad del dominio y vida del proyecto.**
|
|
18
|
+
|
|
19
|
+
### Step 0: Elegir enfoque (`when-to-use`)
|
|
20
|
+
|
|
21
|
+
| Señal | Recomendación |
|
|
22
|
+
|-------|---------------|
|
|
23
|
+
| CRUD, prototipos, APIs chicas, dominio fino, equipo chico | **Vertical Slice** (o single project) |
|
|
24
|
+
| Dominio complejo, reglas de negocio ricas, long-lived, varios equipos | **Clean Architecture** |
|
|
25
|
+
| Necesitás aislar el dominio de frameworks/DB para test y reemplazo | **Clean Architecture** |
|
|
26
|
+
| Querés velocidad y bajo overhead de indirección | **Vertical Slice** |
|
|
27
|
+
|
|
28
|
+
> Evitá Clean Architecture para CRUD simple: los proyectos extra y la indirección cuestan más de lo que aportan
|
|
29
|
+
> cuando la lógica de negocio es delgada. Empezá con Vertical Slice y migrá si el dominio crece.
|
|
30
|
+
|
|
31
|
+
### Step 1: Aplicar la estructura
|
|
32
|
+
|
|
33
|
+
- **Vertical Slice:** un folder por feature/use-case; cada slice tiene su request, handler, validator y endpoint juntos. Ver `references/project-structure.md`.
|
|
34
|
+
- **Clean Architecture:** proyectos `Domain → Application → Infrastructure → Api`; las dependencias apuntan **hacia adentro**, nunca al revés. Ver `references/project-structure.md`.
|
|
35
|
+
|
|
36
|
+
### Step 2: Patrones idiomáticos
|
|
37
|
+
|
|
38
|
+
Leer `references/patterns.md`: MediatR/handlers por use-case, `IApplicationDbContext` (exponer EF Core sin repos
|
|
39
|
+
genéricos), pipeline behaviors (validación/logging/transacción), Result pattern en vez de excepciones para flujo,
|
|
40
|
+
y outbox pattern para consistencia entre DB y eventos.
|
|
41
|
+
|
|
42
|
+
### Step 3: Revisar contra anti-patterns + checklist
|
|
43
|
+
|
|
44
|
+
- `references/anti-patterns.md` — repos genéricos sobre EF Core, fat controllers, dependencias que apuntan hacia afuera, lógica de dominio en Infrastructure.
|
|
45
|
+
- `references/checklist.md` — verificación de cierre antes de dar la estructura por buena.
|
|
46
|
+
|
|
47
|
+
Antes de aplicar una reestructuración significativa, confirmar el approach con el usuario (Question → Decision → Approval). Si todo el checklist pasa → arquitectura **COMPLIANT**.
|
|
48
|
+
|
|
49
|
+
## Resumen de enfoques
|
|
50
|
+
|
|
51
|
+
| Aspecto | Vertical Slice | Clean Architecture |
|
|
52
|
+
|---------|----------------|--------------------|
|
|
53
|
+
| Eje de organización | Feature / use-case | Capa técnica |
|
|
54
|
+
| Proyectos | 1 (+ tests) | 4 (Domain/App/Infra/Api) |
|
|
55
|
+
| Acoplamiento entre features | Bajo (slices independientes) | Mediado por Application |
|
|
56
|
+
| Overhead inicial | Bajo | Alto |
|
|
57
|
+
| Mejor para | CRUD, features, velocidad | Dominios complejos, long-lived |
|
|
58
|
+
| Riesgo | Duplicación entre slices | Sobre-ingeniería |
|
|
59
|
+
|
|
60
|
+
## Próximos pasos
|
|
61
|
+
|
|
62
|
+
Definida la arquitectura → `/dotnet-scaffold` para generar el proyecto, y `/dotnet-best-practices` para revisar el código contra las 40 reglas.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
_Inspirado en [ardalis/CleanArchitecture](https://github.com/ardalis/CleanArchitecture), [nadirbad/VerticalSliceArchitecture](https://github.com/nadirbad/VerticalSliceArchitecture) y la guía de Vertical Slice de [Milan Jovanović](https://www.milanjovanovic.tech/blog/vertical-slice-architecture-dotnet). Adaptado al formato Arcane._
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Anti-Patterns
|
|
2
|
+
|
|
3
|
+
- **`IRepository<T>` / `IUnitOfWork` genérico sobre EF Core** — el `DbContext` ya es repositorio (`DbSet<>`) y Unit of Work (`SaveChangesAsync`); envolverlo agrega indirección sin valor y te tapa LINQ/Include/proyecciones.
|
|
4
|
+
- **Fat controllers / endpoints con lógica de negocio** — el endpoint solo traduce HTTP ↔ request y mapea el resultado; las reglas van en el handler/dominio, no en el controlador.
|
|
5
|
+
- **Dependencias apuntando hacia afuera** — `Domain` referenciando `Infrastructure` o EF Core rompe la regla de dependencias: el dominio deja de ser testeable y reemplazable.
|
|
6
|
+
- **Application anémica que solo reenvía a repos** — si el handler no hace más que `return repo.Get(id)`, la capa no aporta nada; movés lógica de negocio real al dominio/handler o eliminás la indirección.
|
|
7
|
+
- **Filtrar entidades EF como respuestas de API** — exponer entidades de persistencia acopla el contrato HTTP al schema de la DB y arrastra lazy-loading/ciclos; devolvé DTOs en el borde.
|
|
8
|
+
- **Forzar Clean Architecture en un CRUD simple** — cuatro proyectos y pipeline behaviors para listar y guardar registros es sobre-ingeniería; empezá con Vertical Slice o single project.
|
|
9
|
+
- **Service locator estático / `BuildServiceProvider()` en `Program.cs`** — construir el provider a mano crea grafos duplicados y dependencias ocultas; registrá todo en el contenedor y dejá que resuelva por constructor.
|
|
10
|
+
- **Un `Services/` y `Controllers/` gigantes** — organizar por tipo técnico dispersa cada feature en cinco carpetas; organizá por feature/slice para que un caso de uso viva junto.
|
|
11
|
+
|
|
12
|
+
_Ref: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-dotnet · https://github.com/ardalis/CleanArchitecture · https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/_
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Checklist
|
|
2
|
+
|
|
3
|
+
Verificá antes de dar la estructura por buena.
|
|
4
|
+
|
|
5
|
+
- [ ] Las dependencias apuntan hacia adentro: `Api → Infrastructure → Application → Domain`
|
|
6
|
+
- [ ] `Domain` no tiene `<ProjectReference>` ni paquetes de framework (sin EF, sin ASP.NET)
|
|
7
|
+
- [ ] No hay `IRepository<T>` / `IUnitOfWork` genérico envolviendo EF Core
|
|
8
|
+
- [ ] EF Core se expone vía `IApplicationDbContext` en Application, implementado en Infrastructure
|
|
9
|
+
- [ ] En Vertical Slice, cada slice es autocontenido (request + handler + validator + endpoint)
|
|
10
|
+
- [ ] Los slices no dependen entre sí (acoplamiento solo vía `Common`/`Infrastructure`)
|
|
11
|
+
- [ ] DTOs en el borde: ninguna entidad EF se devuelve como respuesta de API
|
|
12
|
+
- [ ] Cada handler tiene una sola responsabilidad (un caso de uso por archivo)
|
|
13
|
+
- [ ] `Program.cs` es solo composition root (DI + pipeline + map endpoints), sin lógica de negocio
|
|
14
|
+
- [ ] La validación corre en un pipeline behavior, no repetida en cada endpoint
|
|
15
|
+
- [ ] Fallos esperados se modelan con `Result<T>`/`ErrorOr`, no con excepciones de flujo
|
|
16
|
+
- [ ] Las migraciones EF Core están versionadas en git
|
|
17
|
+
- [ ] El enfoque elegido (VSA vs Clean) corresponde a la complejidad real del dominio
|
|
18
|
+
|
|
19
|
+
_Ref: https://github.com/ardalis/CleanArchitecture · https://github.com/nadirbad/VerticalSliceArchitecture · https://www.milanjovanovic.tech/blog/clean-architecture-folder-structure_
|
|
@@ -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_
|