claudient 0.1.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/.claude-plugin/plugin.json +42 -0
- package/CONTEXT.md +58 -0
- package/README.md +165 -0
- package/agents/build-resolvers/de/python-resolver.md +64 -0
- package/agents/build-resolvers/de/typescript-resolver.md +65 -0
- package/agents/build-resolvers/es/python-resolver.md +64 -0
- package/agents/build-resolvers/es/typescript-resolver.md +65 -0
- package/agents/build-resolvers/fr/python-resolver.md +64 -0
- package/agents/build-resolvers/fr/typescript-resolver.md +65 -0
- package/agents/build-resolvers/nl/python-resolver.md +64 -0
- package/agents/build-resolvers/nl/typescript-resolver.md +65 -0
- package/agents/build-resolvers/python-resolver.md +62 -0
- package/agents/build-resolvers/typescript-resolver.md +63 -0
- package/agents/core/architect.md +64 -0
- package/agents/core/code-reviewer.md +78 -0
- package/agents/core/de/architect.md +66 -0
- package/agents/core/de/code-reviewer.md +80 -0
- package/agents/core/de/planner.md +63 -0
- package/agents/core/de/security-reviewer.md +93 -0
- package/agents/core/es/architect.md +66 -0
- package/agents/core/es/code-reviewer.md +80 -0
- package/agents/core/es/planner.md +63 -0
- package/agents/core/es/security-reviewer.md +93 -0
- package/agents/core/fr/architect.md +66 -0
- package/agents/core/fr/code-reviewer.md +80 -0
- package/agents/core/fr/planner.md +63 -0
- package/agents/core/fr/security-reviewer.md +93 -0
- package/agents/core/nl/architect.md +66 -0
- package/agents/core/nl/code-reviewer.md +80 -0
- package/agents/core/nl/planner.md +63 -0
- package/agents/core/nl/security-reviewer.md +93 -0
- package/agents/core/planner.md +61 -0
- package/agents/core/security-reviewer.md +91 -0
- package/guides/agent-orchestration.md +231 -0
- package/guides/de/agent-orchestration.md +174 -0
- package/guides/de/getting-started.md +164 -0
- package/guides/de/hooks-cookbook.md +160 -0
- package/guides/de/memory-management.md +153 -0
- package/guides/de/security.md +180 -0
- package/guides/de/skill-authoring.md +214 -0
- package/guides/de/token-optimization.md +156 -0
- package/guides/es/agent-orchestration.md +174 -0
- package/guides/es/getting-started.md +164 -0
- package/guides/es/hooks-cookbook.md +160 -0
- package/guides/es/memory-management.md +153 -0
- package/guides/es/security.md +180 -0
- package/guides/es/skill-authoring.md +214 -0
- package/guides/es/token-optimization.md +156 -0
- package/guides/fr/agent-orchestration.md +174 -0
- package/guides/fr/getting-started.md +164 -0
- package/guides/fr/hooks-cookbook.md +227 -0
- package/guides/fr/memory-management.md +169 -0
- package/guides/fr/security.md +180 -0
- package/guides/fr/skill-authoring.md +214 -0
- package/guides/fr/token-optimization.md +158 -0
- package/guides/getting-started.md +164 -0
- package/guides/hooks-cookbook.md +423 -0
- package/guides/memory-management.md +192 -0
- package/guides/nl/agent-orchestration.md +174 -0
- package/guides/nl/getting-started.md +164 -0
- package/guides/nl/hooks-cookbook.md +160 -0
- package/guides/nl/memory-management.md +153 -0
- package/guides/nl/security.md +180 -0
- package/guides/nl/skill-authoring.md +214 -0
- package/guides/nl/token-optimization.md +156 -0
- package/guides/security.md +229 -0
- package/guides/skill-authoring.md +226 -0
- package/guides/token-optimization.md +169 -0
- package/hooks/lifecycle/cost-tracker.md +49 -0
- package/hooks/lifecycle/cost-tracker.sh +59 -0
- package/hooks/lifecycle/pre-compact-save.md +56 -0
- package/hooks/lifecycle/pre-compact-save.sh +37 -0
- package/hooks/lifecycle/session-start.md +50 -0
- package/hooks/lifecycle/session-start.sh +47 -0
- package/hooks/post-tool-use/audit-log.md +53 -0
- package/hooks/post-tool-use/audit-log.sh +53 -0
- package/hooks/post-tool-use/prettier.md +53 -0
- package/hooks/post-tool-use/prettier.sh +49 -0
- package/hooks/pre-tool-use/block-dangerous.md +48 -0
- package/hooks/pre-tool-use/block-dangerous.sh +76 -0
- package/hooks/pre-tool-use/git-push-confirm.md +46 -0
- package/hooks/pre-tool-use/git-push-confirm.sh +36 -0
- package/mcp/configs/github.json +11 -0
- package/mcp/configs/postgres.json +11 -0
- package/mcp/de/recommended-servers.md +170 -0
- package/mcp/es/recommended-servers.md +170 -0
- package/mcp/fr/recommended-servers.md +170 -0
- package/mcp/nl/recommended-servers.md +170 -0
- package/mcp/recommended-servers.md +168 -0
- package/package.json +45 -0
- package/prompts/project-starters/de/fastapi-project.md +62 -0
- package/prompts/project-starters/de/nextjs-project.md +82 -0
- package/prompts/project-starters/es/fastapi-project.md +62 -0
- package/prompts/project-starters/es/nextjs-project.md +82 -0
- package/prompts/project-starters/fastapi-project.md +60 -0
- package/prompts/project-starters/fr/fastapi-project.md +62 -0
- package/prompts/project-starters/fr/nextjs-project.md +82 -0
- package/prompts/project-starters/nextjs-project.md +80 -0
- package/prompts/project-starters/nl/fastapi-project.md +62 -0
- package/prompts/project-starters/nl/nextjs-project.md +82 -0
- package/prompts/system-prompts/ai-product.md +80 -0
- package/prompts/system-prompts/data-pipeline.md +76 -0
- package/prompts/system-prompts/de/ai-product.md +82 -0
- package/prompts/system-prompts/de/data-pipeline.md +78 -0
- package/prompts/system-prompts/de/saas-backend.md +71 -0
- package/prompts/system-prompts/es/ai-product.md +82 -0
- package/prompts/system-prompts/es/data-pipeline.md +78 -0
- package/prompts/system-prompts/es/saas-backend.md +71 -0
- package/prompts/system-prompts/fr/ai-product.md +82 -0
- package/prompts/system-prompts/fr/data-pipeline.md +78 -0
- package/prompts/system-prompts/fr/saas-backend.md +71 -0
- package/prompts/system-prompts/nl/ai-product.md +82 -0
- package/prompts/system-prompts/nl/data-pipeline.md +78 -0
- package/prompts/system-prompts/nl/saas-backend.md +71 -0
- package/prompts/system-prompts/saas-backend.md +69 -0
- package/prompts/task-specific/changelog.md +81 -0
- package/prompts/task-specific/de/changelog.md +83 -0
- package/prompts/task-specific/de/debugging.md +78 -0
- package/prompts/task-specific/de/pr-description.md +69 -0
- package/prompts/task-specific/debugging.md +76 -0
- package/prompts/task-specific/es/changelog.md +83 -0
- package/prompts/task-specific/es/debugging.md +78 -0
- package/prompts/task-specific/es/pr-description.md +69 -0
- package/prompts/task-specific/fr/changelog.md +83 -0
- package/prompts/task-specific/fr/debugging.md +78 -0
- package/prompts/task-specific/fr/pr-description.md +69 -0
- package/prompts/task-specific/nl/changelog.md +83 -0
- package/prompts/task-specific/nl/debugging.md +78 -0
- package/prompts/task-specific/nl/pr-description.md +69 -0
- package/prompts/task-specific/pr-description.md +67 -0
- package/rules/common/coding-style.md +45 -0
- package/rules/common/de/coding-style.md +47 -0
- package/rules/common/de/git.md +48 -0
- package/rules/common/de/performance.md +40 -0
- package/rules/common/de/security.md +45 -0
- package/rules/common/de/testing.md +45 -0
- package/rules/common/es/coding-style.md +47 -0
- package/rules/common/es/git.md +48 -0
- package/rules/common/es/performance.md +40 -0
- package/rules/common/es/security.md +45 -0
- package/rules/common/es/testing.md +45 -0
- package/rules/common/fr/coding-style.md +47 -0
- package/rules/common/fr/git.md +48 -0
- package/rules/common/fr/performance.md +40 -0
- package/rules/common/fr/security.md +45 -0
- package/rules/common/fr/testing.md +45 -0
- package/rules/common/git.md +46 -0
- package/rules/common/nl/coding-style.md +47 -0
- package/rules/common/nl/git.md +48 -0
- package/rules/common/nl/performance.md +40 -0
- package/rules/common/nl/security.md +45 -0
- package/rules/common/nl/testing.md +45 -0
- package/rules/common/performance.md +38 -0
- package/rules/common/security.md +43 -0
- package/rules/common/testing.md +43 -0
- package/rules/language-specific/de/go.md +48 -0
- package/rules/language-specific/de/python.md +38 -0
- package/rules/language-specific/de/typescript.md +51 -0
- package/rules/language-specific/es/go.md +48 -0
- package/rules/language-specific/es/python.md +38 -0
- package/rules/language-specific/es/typescript.md +51 -0
- package/rules/language-specific/fr/go.md +48 -0
- package/rules/language-specific/fr/python.md +38 -0
- package/rules/language-specific/fr/typescript.md +51 -0
- package/rules/language-specific/go.md +46 -0
- package/rules/language-specific/nl/go.md +48 -0
- package/rules/language-specific/nl/python.md +38 -0
- package/rules/language-specific/nl/typescript.md +51 -0
- package/rules/language-specific/python.md +36 -0
- package/rules/language-specific/typescript.md +49 -0
- package/scripts/cli.js +161 -0
- package/scripts/link-skills.sh +35 -0
- package/scripts/list-skills.sh +34 -0
- package/skills/ai-engineering/agent-construction.md +285 -0
- package/skills/ai-engineering/claude-api.md +248 -0
- package/skills/ai-engineering/de/agent-construction.md +287 -0
- package/skills/ai-engineering/de/claude-api.md +250 -0
- package/skills/ai-engineering/es/agent-construction.md +287 -0
- package/skills/ai-engineering/es/claude-api.md +250 -0
- package/skills/ai-engineering/fr/agent-construction.md +287 -0
- package/skills/ai-engineering/fr/claude-api.md +250 -0
- package/skills/ai-engineering/nl/agent-construction.md +287 -0
- package/skills/ai-engineering/nl/claude-api.md +250 -0
- package/skills/backend/dotnet/csharp.md +304 -0
- package/skills/backend/dotnet/de/csharp.md +306 -0
- package/skills/backend/dotnet/es/csharp.md +306 -0
- package/skills/backend/dotnet/fr/csharp.md +306 -0
- package/skills/backend/dotnet/nl/csharp.md +306 -0
- package/skills/backend/go/de/go.md +307 -0
- package/skills/backend/go/es/go.md +307 -0
- package/skills/backend/go/fr/go.md +307 -0
- package/skills/backend/go/go.md +305 -0
- package/skills/backend/go/nl/go.md +307 -0
- package/skills/backend/nodejs/de/nestjs.md +274 -0
- package/skills/backend/nodejs/de/nextjs.md +222 -0
- package/skills/backend/nodejs/es/nestjs.md +274 -0
- package/skills/backend/nodejs/es/nextjs.md +222 -0
- package/skills/backend/nodejs/fr/nestjs.md +274 -0
- package/skills/backend/nodejs/fr/nextjs.md +222 -0
- package/skills/backend/nodejs/nestjs.md +272 -0
- package/skills/backend/nodejs/nextjs.md +220 -0
- package/skills/backend/nodejs/nl/nestjs.md +274 -0
- package/skills/backend/nodejs/nl/nextjs.md +222 -0
- package/skills/backend/python/de/django.md +285 -0
- package/skills/backend/python/de/fastapi.md +244 -0
- package/skills/backend/python/django.md +283 -0
- package/skills/backend/python/es/django.md +285 -0
- package/skills/backend/python/es/fastapi.md +244 -0
- package/skills/backend/python/fastapi.md +242 -0
- package/skills/backend/python/fr/django.md +285 -0
- package/skills/backend/python/fr/fastapi.md +244 -0
- package/skills/backend/python/nl/django.md +285 -0
- package/skills/backend/python/nl/fastapi.md +244 -0
- package/skills/data-ml/dbt-data-pipelines.md +155 -0
- package/skills/data-ml/de/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/de/pandas-polars.md +147 -0
- package/skills/data-ml/de/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/es/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/es/pandas-polars.md +147 -0
- package/skills/data-ml/es/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/fr/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/fr/pandas-polars.md +147 -0
- package/skills/data-ml/fr/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/nl/dbt-data-pipelines.md +157 -0
- package/skills/data-ml/nl/pandas-polars.md +147 -0
- package/skills/data-ml/nl/pytorch-tensorflow.md +171 -0
- package/skills/data-ml/pandas-polars.md +145 -0
- package/skills/data-ml/pytorch-tensorflow.md +169 -0
- package/skills/database/de/graphql.md +181 -0
- package/skills/database/es/graphql.md +181 -0
- package/skills/database/fr/graphql.md +181 -0
- package/skills/database/graphql.md +179 -0
- package/skills/database/nl/graphql.md +181 -0
- package/skills/devops-infra/de/docker.md +133 -0
- package/skills/devops-infra/de/github-actions.md +179 -0
- package/skills/devops-infra/de/kubernetes.md +129 -0
- package/skills/devops-infra/de/terraform.md +130 -0
- package/skills/devops-infra/docker.md +131 -0
- package/skills/devops-infra/es/docker.md +133 -0
- package/skills/devops-infra/es/github-actions.md +179 -0
- package/skills/devops-infra/es/kubernetes.md +129 -0
- package/skills/devops-infra/es/terraform.md +130 -0
- package/skills/devops-infra/fr/docker.md +133 -0
- package/skills/devops-infra/fr/github-actions.md +179 -0
- package/skills/devops-infra/fr/kubernetes.md +129 -0
- package/skills/devops-infra/fr/terraform.md +130 -0
- package/skills/devops-infra/github-actions.md +177 -0
- package/skills/devops-infra/kubernetes.md +127 -0
- package/skills/devops-infra/nl/docker.md +133 -0
- package/skills/devops-infra/nl/github-actions.md +179 -0
- package/skills/devops-infra/nl/kubernetes.md +129 -0
- package/skills/devops-infra/nl/terraform.md +130 -0
- package/skills/devops-infra/terraform.md +128 -0
- package/skills/finance-payments/de/stripe.md +187 -0
- package/skills/finance-payments/es/stripe.md +187 -0
- package/skills/finance-payments/fr/stripe.md +187 -0
- package/skills/finance-payments/nl/stripe.md +187 -0
- package/skills/finance-payments/stripe.md +185 -0
- package/workflows/code-review.md +151 -0
- package/workflows/de/code-review.md +153 -0
- package/workflows/de/debugging-session.md +146 -0
- package/workflows/de/feature-development.md +155 -0
- package/workflows/de/new-project-bootstrap.md +175 -0
- package/workflows/de/refactor-safely.md +150 -0
- package/workflows/debugging-session.md +144 -0
- package/workflows/es/code-review.md +153 -0
- package/workflows/es/debugging-session.md +146 -0
- package/workflows/es/feature-development.md +155 -0
- package/workflows/es/new-project-bootstrap.md +175 -0
- package/workflows/es/refactor-safely.md +150 -0
- package/workflows/feature-development.md +153 -0
- package/workflows/fr/code-review.md +153 -0
- package/workflows/fr/debugging-session.md +146 -0
- package/workflows/fr/feature-development.md +155 -0
- package/workflows/fr/new-project-bootstrap.md +175 -0
- package/workflows/fr/refactor-safely.md +150 -0
- package/workflows/new-project-bootstrap.md +173 -0
- package/workflows/nl/code-review.md +153 -0
- package/workflows/nl/debugging-session.md +146 -0
- package/workflows/nl/feature-development.md +155 -0
- package/workflows/nl/new-project-bootstrap.md +175 -0
- package/workflows/nl/refactor-safely.md +150 -0
- package/workflows/refactor-safely.md +148 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
> 🇩🇪 Dies ist die deutsche Übersetzung. [Englische Version](../csharp.md).
|
|
2
|
+
|
|
3
|
+
# C#/.NET Skill
|
|
4
|
+
|
|
5
|
+
## Wann aktivieren
|
|
6
|
+
- Eine .NET Web API bauen (minimale API oder controller-basiert)
|
|
7
|
+
- Entity Framework Core mit Migrationen einrichten
|
|
8
|
+
- Den .NET Dependency Injection-Container konfigurieren
|
|
9
|
+
- Hintergrunddienste mit `IHostedService` oder `BackgroundService` schreiben
|
|
10
|
+
- Middleware-Pipeline-Komponenten implementieren
|
|
11
|
+
- LINQ-Abfragen schreiben und deferred Execution verstehen
|
|
12
|
+
- async/await in ASP.NET Core korrekt einrichten
|
|
13
|
+
|
|
14
|
+
## Wann NICHT verwenden
|
|
15
|
+
- Node.js- oder Python-Dienste
|
|
16
|
+
- .NET Framework (vor .NET 5) Legacy-Codebasen — Muster unterscheiden sich
|
|
17
|
+
- Blazor oder MAUI-Frontend — andere Belange
|
|
18
|
+
- Unity-Spielentwicklung — andere Laufzeitumgebung
|
|
19
|
+
|
|
20
|
+
## Anweisungen
|
|
21
|
+
|
|
22
|
+
### Projektstruktur
|
|
23
|
+
```
|
|
24
|
+
MyApi/
|
|
25
|
+
├── MyApi.sln
|
|
26
|
+
├── src/
|
|
27
|
+
│ └── MyApi/
|
|
28
|
+
│ ├── Program.cs # Einstiegspunkt + DI-Container
|
|
29
|
+
│ ├── appsettings.json
|
|
30
|
+
│ ├── appsettings.Development.json
|
|
31
|
+
│ ├── Controllers/ # Controller-basierte API
|
|
32
|
+
│ ├── Endpoints/ # Minimale API-Erweiterungen
|
|
33
|
+
│ ├── Models/ # EF Core-Entitäten
|
|
34
|
+
│ ├── DTOs/ # Anfrage-/Antwortformen
|
|
35
|
+
│ ├── Services/ # Business-Logik-Interfaces + Implementierungen
|
|
36
|
+
│ ├── Data/
|
|
37
|
+
│ │ └── AppDbContext.cs
|
|
38
|
+
│ └── Middleware/
|
|
39
|
+
└── tests/
|
|
40
|
+
└── MyApi.Tests/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Program.cs — minimales API-Setup
|
|
44
|
+
```csharp
|
|
45
|
+
// Program.cs — .NET 6+ Top-Level-Statements + minimale API
|
|
46
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
47
|
+
|
|
48
|
+
// Services registrieren
|
|
49
|
+
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
50
|
+
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
|
|
51
|
+
|
|
52
|
+
builder.Services.AddScoped<IUserService, UserService>();
|
|
53
|
+
builder.Services.AddControllers();
|
|
54
|
+
builder.Services.AddEndpointsApiExplorer();
|
|
55
|
+
builder.Services.AddSwaggerGen();
|
|
56
|
+
|
|
57
|
+
var app = builder.Build();
|
|
58
|
+
|
|
59
|
+
if (app.Environment.IsDevelopment())
|
|
60
|
+
{
|
|
61
|
+
app.UseSwagger();
|
|
62
|
+
app.UseSwaggerUI();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
app.UseHttpsRedirection();
|
|
66
|
+
app.UseAuthentication();
|
|
67
|
+
app.UseAuthorization();
|
|
68
|
+
app.MapControllers();
|
|
69
|
+
|
|
70
|
+
// Minimale API-Endpunkte
|
|
71
|
+
app.MapGroup("/api/v1").MapUserEndpoints();
|
|
72
|
+
|
|
73
|
+
app.Run();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### EF Core-Entity + DbContext
|
|
77
|
+
```csharp
|
|
78
|
+
// Models/User.cs
|
|
79
|
+
public class User
|
|
80
|
+
{
|
|
81
|
+
public Guid Id { get; set; } = Guid.NewGuid();
|
|
82
|
+
public required string Email { get; set; }
|
|
83
|
+
public required string PasswordHash { get; set; }
|
|
84
|
+
public bool IsActive { get; set; } = true;
|
|
85
|
+
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
|
86
|
+
|
|
87
|
+
public ICollection<Post> Posts { get; set; } = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Data/AppDbContext.cs
|
|
91
|
+
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
|
92
|
+
{
|
|
93
|
+
public DbSet<User> Users => Set<User>();
|
|
94
|
+
public DbSet<Post> Posts => Set<Post>();
|
|
95
|
+
|
|
96
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
97
|
+
{
|
|
98
|
+
modelBuilder.Entity<User>(e =>
|
|
99
|
+
{
|
|
100
|
+
e.HasIndex(u => u.Email).IsUnique();
|
|
101
|
+
e.Property(u => u.Email).HasMaxLength(320);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Dependency Injection — Services
|
|
108
|
+
```csharp
|
|
109
|
+
// Services/IUserService.cs
|
|
110
|
+
public interface IUserService
|
|
111
|
+
{
|
|
112
|
+
Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
113
|
+
Task<UserDto> CreateAsync(CreateUserRequest request, CancellationToken ct = default);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Services/UserService.cs
|
|
117
|
+
public class UserService(AppDbContext db) : IUserService
|
|
118
|
+
{
|
|
119
|
+
public async Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
120
|
+
{
|
|
121
|
+
var user = await db.Users
|
|
122
|
+
.AsNoTracking()
|
|
123
|
+
.FirstOrDefaultAsync(u => u.Id == id, ct)
|
|
124
|
+
?? throw new NotFoundException($"User {id} not found");
|
|
125
|
+
|
|
126
|
+
return new UserDto(user.Id, user.Email, user.CreatedAt);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async Task<UserDto> CreateAsync(CreateUserRequest request, CancellationToken ct = default)
|
|
130
|
+
{
|
|
131
|
+
if (await db.Users.AnyAsync(u => u.Email == request.Email, ct))
|
|
132
|
+
throw new ConflictException("Email already in use");
|
|
133
|
+
|
|
134
|
+
var user = new User
|
|
135
|
+
{
|
|
136
|
+
Email = request.Email,
|
|
137
|
+
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password)
|
|
138
|
+
};
|
|
139
|
+
db.Users.Add(user);
|
|
140
|
+
await db.SaveChangesAsync(ct);
|
|
141
|
+
return new UserDto(user.Id, user.Email, user.CreatedAt);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Controller-basierte API
|
|
147
|
+
```csharp
|
|
148
|
+
[ApiController]
|
|
149
|
+
[Route("api/v1/[controller]")]
|
|
150
|
+
public class UsersController(IUserService userService) : ControllerBase
|
|
151
|
+
{
|
|
152
|
+
[HttpGet("{id:guid}")]
|
|
153
|
+
[ProducesResponseType<UserDto>(StatusCodes.Status200OK)]
|
|
154
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
155
|
+
public async Task<IActionResult> GetUser(Guid id, CancellationToken ct)
|
|
156
|
+
{
|
|
157
|
+
var user = await userService.GetByIdAsync(id, ct);
|
|
158
|
+
return Ok(user);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
[HttpPost]
|
|
162
|
+
[ProducesResponseType<UserDto>(StatusCodes.Status201Created)]
|
|
163
|
+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
164
|
+
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
|
165
|
+
public async Task<IActionResult> CreateUser(
|
|
166
|
+
[FromBody] CreateUserRequest request,
|
|
167
|
+
CancellationToken ct)
|
|
168
|
+
{
|
|
169
|
+
var user = await userService.CreateAsync(request, ct);
|
|
170
|
+
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Minimale API-Endpunkte (Erweiterungsmethoden-Muster)
|
|
176
|
+
```csharp
|
|
177
|
+
// Endpoints/UserEndpoints.cs
|
|
178
|
+
public static class UserEndpoints
|
|
179
|
+
{
|
|
180
|
+
public static RouteGroupBuilder MapUserEndpoints(this RouteGroupBuilder group)
|
|
181
|
+
{
|
|
182
|
+
var users = group.MapGroup("/users").WithTags("Users");
|
|
183
|
+
|
|
184
|
+
users.MapGet("/{id:guid}", async (Guid id, IUserService svc, CancellationToken ct) =>
|
|
185
|
+
{
|
|
186
|
+
var user = await svc.GetByIdAsync(id, ct);
|
|
187
|
+
return Results.Ok(user);
|
|
188
|
+
})
|
|
189
|
+
.WithName("GetUser")
|
|
190
|
+
.Produces<UserDto>();
|
|
191
|
+
|
|
192
|
+
return group;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Middleware
|
|
198
|
+
```csharp
|
|
199
|
+
// Middleware/RequestLoggingMiddleware.cs
|
|
200
|
+
public class RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
|
|
201
|
+
{
|
|
202
|
+
public async Task InvokeAsync(HttpContext context)
|
|
203
|
+
{
|
|
204
|
+
var sw = Stopwatch.StartNew();
|
|
205
|
+
try
|
|
206
|
+
{
|
|
207
|
+
await next(context);
|
|
208
|
+
}
|
|
209
|
+
finally
|
|
210
|
+
{
|
|
211
|
+
sw.Stop();
|
|
212
|
+
logger.LogInformation(
|
|
213
|
+
"{Method} {Path} {StatusCode} in {Elapsed}ms",
|
|
214
|
+
context.Request.Method,
|
|
215
|
+
context.Request.Path,
|
|
216
|
+
context.Response.StatusCode,
|
|
217
|
+
sw.ElapsedMilliseconds);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// In Program.cs vor anderer Middleware registrieren:
|
|
223
|
+
app.UseMiddleware<RequestLoggingMiddleware>();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Hintergrunddienste
|
|
227
|
+
```csharp
|
|
228
|
+
// Services/CleanupService.cs
|
|
229
|
+
public class CleanupService(IServiceProvider services, ILogger<CleanupService> logger)
|
|
230
|
+
: BackgroundService
|
|
231
|
+
{
|
|
232
|
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
233
|
+
{
|
|
234
|
+
while (!stoppingToken.IsCancellationRequested)
|
|
235
|
+
{
|
|
236
|
+
await DoCleanupAsync(stoppingToken);
|
|
237
|
+
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async Task DoCleanupAsync(CancellationToken ct)
|
|
242
|
+
{
|
|
243
|
+
// Neuen Scope für jede Iteration verwenden — BackgroundService ist Singleton
|
|
244
|
+
using var scope = services.CreateScope();
|
|
245
|
+
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
246
|
+
var cutoff = DateTimeOffset.UtcNow.AddDays(-30);
|
|
247
|
+
await db.Sessions.Where(s => s.ExpiresAt < cutoff).ExecuteDeleteAsync(ct);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Registrieren: builder.Services.AddHostedService<CleanupService>();
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### LINQ-Best-Practices
|
|
255
|
+
```csharp
|
|
256
|
+
// AsNoTracking() immer für schreibgeschützte Abfragen verwenden
|
|
257
|
+
var users = await db.Users.AsNoTracking().Where(u => u.IsActive).ToListAsync(ct);
|
|
258
|
+
|
|
259
|
+
// Nur benötigte Spalten auswählen — vollständige Entitäten für Projektionen vermeiden
|
|
260
|
+
var emails = await db.Users
|
|
261
|
+
.Where(u => u.IsActive)
|
|
262
|
+
.Select(u => u.Email)
|
|
263
|
+
.ToListAsync(ct);
|
|
264
|
+
|
|
265
|
+
// ExecuteUpdateAsync/ExecuteDeleteAsync für Bulk-Ops verwenden — überspringt das Laden von Entitäten
|
|
266
|
+
await db.Users
|
|
267
|
+
.Where(u => !u.IsActive && u.CreatedAt < cutoff)
|
|
268
|
+
.ExecuteDeleteAsync(ct);
|
|
269
|
+
|
|
270
|
+
// N+1 vermeiden: Include() für verknüpfte Daten verwenden
|
|
271
|
+
var posts = await db.Posts
|
|
272
|
+
.Include(p => p.Author)
|
|
273
|
+
.Include(p => p.Tags)
|
|
274
|
+
.Where(p => p.Published)
|
|
275
|
+
.AsNoTracking()
|
|
276
|
+
.ToListAsync(ct);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### DTOs und Records
|
|
280
|
+
```csharp
|
|
281
|
+
// Records für unveränderliche DTOs verwenden
|
|
282
|
+
public record UserDto(Guid Id, string Email, DateTimeOffset CreatedAt);
|
|
283
|
+
public record CreateUserRequest(
|
|
284
|
+
[property: Required, EmailAddress] string Email,
|
|
285
|
+
[property: Required, MinLength(8)] string Password
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Antworttypen mit Problem Details (eingebaut in .NET)
|
|
289
|
+
// Results.Problem() zurückgeben oder Ausnahmen werfen, die von Middleware abgefangen werden
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Beispiel
|
|
293
|
+
|
|
294
|
+
**Benutzer:** Eine `BlogPost`-Ressource zu einer .NET Web API hinzufügen: CRUD-Endpunkte, EF Core-Entity, Migrationen und ein Hintergrundjob, der geplante Posts veröffentlicht.
|
|
295
|
+
|
|
296
|
+
**Erwartete Ausgabe:**
|
|
297
|
+
- `Models/BlogPost.cs` — Entity mit `Id`, `Title`, `Body`, `AuthorId` (FK zu User), `PublishedAt` (nullable), `ScheduledFor` (nullable)
|
|
298
|
+
- `DTOs/BlogPostDtos.cs` — `BlogPostDto`-Record, `CreateBlogPostRequest`-Record mit `[Required]`-Validierung
|
|
299
|
+
- `Services/IBlogPostService.cs` + `BlogPostService.cs` — CRUD-Methoden, `GetPendingScheduledAsync` für Hintergrundjob
|
|
300
|
+
- `Controllers/BlogPostsController.cs` — alle CRUD mit korrekten Statuscodes
|
|
301
|
+
- `Services/PostPublisherService.cs` — `BackgroundService`, der jede Minute prüft und fällige Posts veröffentlicht
|
|
302
|
+
- EF Core-Migration: `dotnet ef migrations add AddBlogPosts`
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
> **Mit uns arbeiten:** Claudient wird von [Uitbreiden](https://uitbreiden.com/) unterstützt — wir bauen KI-Produkte und B2B-Lösungen mit Entwickler-Communities. [uitbreiden.com](https://uitbreiden.com/)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
> 🇪🇸 Esta es la traducción en español. [Versión en inglés](../csharp.md).
|
|
2
|
+
|
|
3
|
+
# Skill de C#/.NET
|
|
4
|
+
|
|
5
|
+
## Cuándo activar
|
|
6
|
+
- Construir una Web API en .NET (minimal API o basada en controladores)
|
|
7
|
+
- Configurar Entity Framework Core con migraciones
|
|
8
|
+
- Configurar el contenedor de inyección de dependencias de .NET
|
|
9
|
+
- Escribir servicios en segundo plano con `IHostedService` o `BackgroundService`
|
|
10
|
+
- Implementar componentes del pipeline de middleware
|
|
11
|
+
- Escribir consultas LINQ y entender la ejecución diferida
|
|
12
|
+
- Configurar async/await correctamente en ASP.NET Core
|
|
13
|
+
|
|
14
|
+
## Cuándo NO usar
|
|
15
|
+
- Servicios en Node.js o Python
|
|
16
|
+
- Codebases legacy de .NET Framework (pre-.NET 5) — los patrones difieren
|
|
17
|
+
- Blazor o MAUI frontend — preocupaciones diferentes
|
|
18
|
+
- Desarrollo de juegos con Unity — runtime diferente
|
|
19
|
+
|
|
20
|
+
## Instrucciones
|
|
21
|
+
|
|
22
|
+
### Estructura del proyecto
|
|
23
|
+
```
|
|
24
|
+
MyApi/
|
|
25
|
+
├── MyApi.sln
|
|
26
|
+
├── src/
|
|
27
|
+
│ └── MyApi/
|
|
28
|
+
│ ├── Program.cs # Punto de entrada + contenedor DI
|
|
29
|
+
│ ├── appsettings.json
|
|
30
|
+
│ ├── appsettings.Development.json
|
|
31
|
+
│ ├── Controllers/ # API basada en controladores
|
|
32
|
+
│ ├── Endpoints/ # Extensiones de minimal API
|
|
33
|
+
│ ├── Models/ # Entidades de EF Core
|
|
34
|
+
│ ├── DTOs/ # Formas de solicitud/respuesta
|
|
35
|
+
│ ├── Services/ # Interfaces + implementaciones de lógica de negocio
|
|
36
|
+
│ ├── Data/
|
|
37
|
+
│ │ └── AppDbContext.cs
|
|
38
|
+
│ └── Middleware/
|
|
39
|
+
└── tests/
|
|
40
|
+
└── MyApi.Tests/
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Program.cs — configuración de minimal API
|
|
44
|
+
```csharp
|
|
45
|
+
// Program.cs — sentencias de nivel superior de .NET 6+ + minimal API
|
|
46
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
47
|
+
|
|
48
|
+
// Registrar servicios
|
|
49
|
+
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
50
|
+
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
|
|
51
|
+
|
|
52
|
+
builder.Services.AddScoped<IUserService, UserService>();
|
|
53
|
+
builder.Services.AddControllers();
|
|
54
|
+
builder.Services.AddEndpointsApiExplorer();
|
|
55
|
+
builder.Services.AddSwaggerGen();
|
|
56
|
+
|
|
57
|
+
var app = builder.Build();
|
|
58
|
+
|
|
59
|
+
if (app.Environment.IsDevelopment())
|
|
60
|
+
{
|
|
61
|
+
app.UseSwagger();
|
|
62
|
+
app.UseSwaggerUI();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
app.UseHttpsRedirection();
|
|
66
|
+
app.UseAuthentication();
|
|
67
|
+
app.UseAuthorization();
|
|
68
|
+
app.MapControllers();
|
|
69
|
+
|
|
70
|
+
// Endpoints de minimal API
|
|
71
|
+
app.MapGroup("/api/v1").MapUserEndpoints();
|
|
72
|
+
|
|
73
|
+
app.Run();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Entidad EF Core + DbContext
|
|
77
|
+
```csharp
|
|
78
|
+
// Models/User.cs
|
|
79
|
+
public class User
|
|
80
|
+
{
|
|
81
|
+
public Guid Id { get; set; } = Guid.NewGuid();
|
|
82
|
+
public required string Email { get; set; }
|
|
83
|
+
public required string PasswordHash { get; set; }
|
|
84
|
+
public bool IsActive { get; set; } = true;
|
|
85
|
+
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
|
|
86
|
+
|
|
87
|
+
public ICollection<Post> Posts { get; set; } = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Data/AppDbContext.cs
|
|
91
|
+
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
|
92
|
+
{
|
|
93
|
+
public DbSet<User> Users => Set<User>();
|
|
94
|
+
public DbSet<Post> Posts => Set<Post>();
|
|
95
|
+
|
|
96
|
+
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
97
|
+
{
|
|
98
|
+
modelBuilder.Entity<User>(e =>
|
|
99
|
+
{
|
|
100
|
+
e.HasIndex(u => u.Email).IsUnique();
|
|
101
|
+
e.Property(u => u.Email).HasMaxLength(320);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Inyección de dependencias — servicios
|
|
108
|
+
```csharp
|
|
109
|
+
// Services/IUserService.cs
|
|
110
|
+
public interface IUserService
|
|
111
|
+
{
|
|
112
|
+
Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
113
|
+
Task<UserDto> CreateAsync(CreateUserRequest request, CancellationToken ct = default);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Services/UserService.cs
|
|
117
|
+
public class UserService(AppDbContext db) : IUserService
|
|
118
|
+
{
|
|
119
|
+
public async Task<UserDto> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
120
|
+
{
|
|
121
|
+
var user = await db.Users
|
|
122
|
+
.AsNoTracking()
|
|
123
|
+
.FirstOrDefaultAsync(u => u.Id == id, ct)
|
|
124
|
+
?? throw new NotFoundException($"User {id} not found");
|
|
125
|
+
|
|
126
|
+
return new UserDto(user.Id, user.Email, user.CreatedAt);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async Task<UserDto> CreateAsync(CreateUserRequest request, CancellationToken ct = default)
|
|
130
|
+
{
|
|
131
|
+
if (await db.Users.AnyAsync(u => u.Email == request.Email, ct))
|
|
132
|
+
throw new ConflictException("Email already in use");
|
|
133
|
+
|
|
134
|
+
var user = new User
|
|
135
|
+
{
|
|
136
|
+
Email = request.Email,
|
|
137
|
+
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password)
|
|
138
|
+
};
|
|
139
|
+
db.Users.Add(user);
|
|
140
|
+
await db.SaveChangesAsync(ct);
|
|
141
|
+
return new UserDto(user.Id, user.Email, user.CreatedAt);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### API basada en controladores
|
|
147
|
+
```csharp
|
|
148
|
+
[ApiController]
|
|
149
|
+
[Route("api/v1/[controller]")]
|
|
150
|
+
public class UsersController(IUserService userService) : ControllerBase
|
|
151
|
+
{
|
|
152
|
+
[HttpGet("{id:guid}")]
|
|
153
|
+
[ProducesResponseType<UserDto>(StatusCodes.Status200OK)]
|
|
154
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
155
|
+
public async Task<IActionResult> GetUser(Guid id, CancellationToken ct)
|
|
156
|
+
{
|
|
157
|
+
var user = await userService.GetByIdAsync(id, ct);
|
|
158
|
+
return Ok(user);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
[HttpPost]
|
|
162
|
+
[ProducesResponseType<UserDto>(StatusCodes.Status201Created)]
|
|
163
|
+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
164
|
+
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
|
165
|
+
public async Task<IActionResult> CreateUser(
|
|
166
|
+
[FromBody] CreateUserRequest request,
|
|
167
|
+
CancellationToken ct)
|
|
168
|
+
{
|
|
169
|
+
var user = await userService.CreateAsync(request, ct);
|
|
170
|
+
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Endpoints de minimal API (patrón de método de extensión)
|
|
176
|
+
```csharp
|
|
177
|
+
// Endpoints/UserEndpoints.cs
|
|
178
|
+
public static class UserEndpoints
|
|
179
|
+
{
|
|
180
|
+
public static RouteGroupBuilder MapUserEndpoints(this RouteGroupBuilder group)
|
|
181
|
+
{
|
|
182
|
+
var users = group.MapGroup("/users").WithTags("Users");
|
|
183
|
+
|
|
184
|
+
users.MapGet("/{id:guid}", async (Guid id, IUserService svc, CancellationToken ct) =>
|
|
185
|
+
{
|
|
186
|
+
var user = await svc.GetByIdAsync(id, ct);
|
|
187
|
+
return Results.Ok(user);
|
|
188
|
+
})
|
|
189
|
+
.WithName("GetUser")
|
|
190
|
+
.Produces<UserDto>();
|
|
191
|
+
|
|
192
|
+
return group;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Middleware
|
|
198
|
+
```csharp
|
|
199
|
+
// Middleware/RequestLoggingMiddleware.cs
|
|
200
|
+
public class RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
|
|
201
|
+
{
|
|
202
|
+
public async Task InvokeAsync(HttpContext context)
|
|
203
|
+
{
|
|
204
|
+
var sw = Stopwatch.StartNew();
|
|
205
|
+
try
|
|
206
|
+
{
|
|
207
|
+
await next(context);
|
|
208
|
+
}
|
|
209
|
+
finally
|
|
210
|
+
{
|
|
211
|
+
sw.Stop();
|
|
212
|
+
logger.LogInformation(
|
|
213
|
+
"{Method} {Path} {StatusCode} in {Elapsed}ms",
|
|
214
|
+
context.Request.Method,
|
|
215
|
+
context.Request.Path,
|
|
216
|
+
context.Response.StatusCode,
|
|
217
|
+
sw.ElapsedMilliseconds);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Registrar en Program.cs antes de otro middleware:
|
|
223
|
+
app.UseMiddleware<RequestLoggingMiddleware>();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Servicios en segundo plano
|
|
227
|
+
```csharp
|
|
228
|
+
// Services/CleanupService.cs
|
|
229
|
+
public class CleanupService(IServiceProvider services, ILogger<CleanupService> logger)
|
|
230
|
+
: BackgroundService
|
|
231
|
+
{
|
|
232
|
+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
233
|
+
{
|
|
234
|
+
while (!stoppingToken.IsCancellationRequested)
|
|
235
|
+
{
|
|
236
|
+
await DoCleanupAsync(stoppingToken);
|
|
237
|
+
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async Task DoCleanupAsync(CancellationToken ct)
|
|
242
|
+
{
|
|
243
|
+
// Usar un nuevo scope para cada iteración — BackgroundService es singleton
|
|
244
|
+
using var scope = services.CreateScope();
|
|
245
|
+
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
246
|
+
var cutoff = DateTimeOffset.UtcNow.AddDays(-30);
|
|
247
|
+
await db.Sessions.Where(s => s.ExpiresAt < cutoff).ExecuteDeleteAsync(ct);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Registrar: builder.Services.AddHostedService<CleanupService>();
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Mejores prácticas de LINQ
|
|
255
|
+
```csharp
|
|
256
|
+
// Siempre usar AsNoTracking() para consultas de solo lectura
|
|
257
|
+
var users = await db.Users.AsNoTracking().Where(u => u.IsActive).ToListAsync(ct);
|
|
258
|
+
|
|
259
|
+
// Seleccionar solo las columnas necesarias — evitar cargar entidades completas para proyecciones
|
|
260
|
+
var emails = await db.Users
|
|
261
|
+
.Where(u => u.IsActive)
|
|
262
|
+
.Select(u => u.Email)
|
|
263
|
+
.ToListAsync(ct);
|
|
264
|
+
|
|
265
|
+
// Usar ExecuteUpdateAsync/ExecuteDeleteAsync para operaciones masivas — evita cargar entidades
|
|
266
|
+
await db.Users
|
|
267
|
+
.Where(u => !u.IsActive && u.CreatedAt < cutoff)
|
|
268
|
+
.ExecuteDeleteAsync(ct);
|
|
269
|
+
|
|
270
|
+
// Evitar N+1: usar Include() para datos relacionados
|
|
271
|
+
var posts = await db.Posts
|
|
272
|
+
.Include(p => p.Author)
|
|
273
|
+
.Include(p => p.Tags)
|
|
274
|
+
.Where(p => p.Published)
|
|
275
|
+
.AsNoTracking()
|
|
276
|
+
.ToListAsync(ct);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### DTOs y records
|
|
280
|
+
```csharp
|
|
281
|
+
// Usar records para DTOs inmutables
|
|
282
|
+
public record UserDto(Guid Id, string Email, DateTimeOffset CreatedAt);
|
|
283
|
+
public record CreateUserRequest(
|
|
284
|
+
[property: Required, EmailAddress] string Email,
|
|
285
|
+
[property: Required, MinLength(8)] string Password
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Tipos de respuesta con problem details (integrado en .NET)
|
|
289
|
+
// Devolver Results.Problem() o lanzar excepciones capturadas por el middleware
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Ejemplo
|
|
293
|
+
|
|
294
|
+
**Usuario:** Agregar un recurso `BlogPost` a una Web API de .NET: endpoints CRUD, entidad EF Core, migraciones y un trabajo en segundo plano que publica posts programados.
|
|
295
|
+
|
|
296
|
+
**Salida esperada:**
|
|
297
|
+
- `Models/BlogPost.cs` — entidad con `Id`, `Title`, `Body`, `AuthorId` (FK a User), `PublishedAt` (nullable), `ScheduledFor` (nullable)
|
|
298
|
+
- `DTOs/BlogPostDtos.cs` — record `BlogPostDto`, record `CreateBlogPostRequest` con validación `[Required]`
|
|
299
|
+
- `Services/IBlogPostService.cs` + `BlogPostService.cs` — métodos CRUD, `GetPendingScheduledAsync` para el trabajo en segundo plano
|
|
300
|
+
- `Controllers/BlogPostsController.cs` — todo CRUD con códigos de estado apropiados
|
|
301
|
+
- `Services/PostPublisherService.cs` — `BackgroundService` que verifica cada minuto y publica los posts pendientes
|
|
302
|
+
- Migración EF Core: `dotnet ef migrations add AddBlogPosts`
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
> **Trabaja con nosotros:** Claudient está respaldado por [Uitbreiden](https://uitbreiden.com/) — construimos productos de IA y soluciones B2B con comunidades de desarrolladores. [uitbreiden.com](https://uitbreiden.com/)
|