autoworkflow 3.1.5 → 3.6.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/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/post-edit.sh +190 -17
- package/.claude/hooks/pre-edit.sh +221 -0
- package/.claude/hooks/session-check.sh +90 -0
- package/.claude/settings.json +56 -6
- package/.claude/settings.local.json +5 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +163 -52
- package/package.json +1 -1
- package/system/triggers.md +256 -17
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# ASP.NET Core Skill
|
|
2
|
+
|
|
3
|
+
## Program.cs (Minimal API)
|
|
4
|
+
\`\`\`csharp
|
|
5
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
6
|
+
|
|
7
|
+
// Add services
|
|
8
|
+
builder.Services.AddControllers();
|
|
9
|
+
builder.Services.AddEndpointsApiExplorer();
|
|
10
|
+
builder.Services.AddSwaggerGen();
|
|
11
|
+
|
|
12
|
+
// Custom services
|
|
13
|
+
builder.Services.AddScoped<IUserService, UserService>();
|
|
14
|
+
builder.Services.AddDbContext<AppDbContext>(options =>
|
|
15
|
+
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
|
|
16
|
+
|
|
17
|
+
// Authentication
|
|
18
|
+
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|
19
|
+
.AddJwtBearer(options =>
|
|
20
|
+
{
|
|
21
|
+
options.TokenValidationParameters = new TokenValidationParameters
|
|
22
|
+
{
|
|
23
|
+
ValidateIssuer = true,
|
|
24
|
+
ValidateAudience = true,
|
|
25
|
+
ValidateLifetime = true,
|
|
26
|
+
ValidIssuer = builder.Configuration["Jwt:Issuer"],
|
|
27
|
+
ValidAudience = builder.Configuration["Jwt:Audience"],
|
|
28
|
+
IssuerSigningKey = new SymmetricSecurityKey(
|
|
29
|
+
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
builder.Services.AddAuthorization();
|
|
34
|
+
|
|
35
|
+
var app = builder.Build();
|
|
36
|
+
|
|
37
|
+
// Middleware pipeline
|
|
38
|
+
if (app.Environment.IsDevelopment())
|
|
39
|
+
{
|
|
40
|
+
app.UseSwagger();
|
|
41
|
+
app.UseSwaggerUI();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
app.UseExceptionHandler("/error");
|
|
45
|
+
app.UseHttpsRedirection();
|
|
46
|
+
app.UseAuthentication();
|
|
47
|
+
app.UseAuthorization();
|
|
48
|
+
|
|
49
|
+
app.MapControllers();
|
|
50
|
+
|
|
51
|
+
app.Run();
|
|
52
|
+
\`\`\`
|
|
53
|
+
|
|
54
|
+
## Controller Pattern
|
|
55
|
+
\`\`\`csharp
|
|
56
|
+
[ApiController]
|
|
57
|
+
[Route("api/v1/[controller]")]
|
|
58
|
+
[Produces("application/json")]
|
|
59
|
+
public class UsersController : ControllerBase
|
|
60
|
+
{
|
|
61
|
+
private readonly IUserService _userService;
|
|
62
|
+
|
|
63
|
+
public UsersController(IUserService userService)
|
|
64
|
+
{
|
|
65
|
+
_userService = userService;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[HttpGet]
|
|
69
|
+
[ProducesResponseType(typeof(PagedResult<UserResponse>), StatusCodes.Status200OK)]
|
|
70
|
+
public async Task<ActionResult<PagedResult<UserResponse>>> GetUsers(
|
|
71
|
+
[FromQuery] int page = 1,
|
|
72
|
+
[FromQuery] int pageSize = 20,
|
|
73
|
+
CancellationToken ct = default)
|
|
74
|
+
{
|
|
75
|
+
var result = await _userService.GetAllAsync(page, pageSize, ct);
|
|
76
|
+
return Ok(result);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[HttpGet("{id}")]
|
|
80
|
+
[ProducesResponseType(typeof(UserResponse), StatusCodes.Status200OK)]
|
|
81
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
82
|
+
public async Task<ActionResult<UserResponse>> GetUser(string id, CancellationToken ct)
|
|
83
|
+
{
|
|
84
|
+
var user = await _userService.GetByIdAsync(id, ct);
|
|
85
|
+
return user is null ? NotFound() : Ok(user);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[HttpPost]
|
|
89
|
+
[ProducesResponseType(typeof(UserResponse), StatusCodes.Status201Created)]
|
|
90
|
+
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
|
91
|
+
public async Task<ActionResult<UserResponse>> CreateUser(
|
|
92
|
+
[FromBody] CreateUserRequest request,
|
|
93
|
+
CancellationToken ct)
|
|
94
|
+
{
|
|
95
|
+
var user = await _userService.CreateAsync(request, ct);
|
|
96
|
+
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
[HttpPut("{id}")]
|
|
100
|
+
[ProducesResponseType(typeof(UserResponse), StatusCodes.Status200OK)]
|
|
101
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
102
|
+
public async Task<ActionResult<UserResponse>> UpdateUser(
|
|
103
|
+
string id,
|
|
104
|
+
[FromBody] UpdateUserRequest request,
|
|
105
|
+
CancellationToken ct)
|
|
106
|
+
{
|
|
107
|
+
var user = await _userService.UpdateAsync(id, request, ct);
|
|
108
|
+
return user is null ? NotFound() : Ok(user);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
[HttpDelete("{id}")]
|
|
112
|
+
[Authorize(Roles = "Admin")]
|
|
113
|
+
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
114
|
+
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
115
|
+
public async Task<IActionResult> DeleteUser(string id, CancellationToken ct)
|
|
116
|
+
{
|
|
117
|
+
var deleted = await _userService.DeleteAsync(id, ct);
|
|
118
|
+
return deleted ? NoContent() : NotFound();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
## Minimal APIs
|
|
124
|
+
\`\`\`csharp
|
|
125
|
+
var app = builder.Build();
|
|
126
|
+
|
|
127
|
+
// Group routes
|
|
128
|
+
var users = app.MapGroup("/api/v1/users")
|
|
129
|
+
.WithTags("Users");
|
|
130
|
+
|
|
131
|
+
users.MapGet("/", async (IUserService service, CancellationToken ct) =>
|
|
132
|
+
await service.GetAllAsync(ct));
|
|
133
|
+
|
|
134
|
+
users.MapGet("/{id}", async (string id, IUserService service, CancellationToken ct) =>
|
|
135
|
+
await service.GetByIdAsync(id, ct) is { } user
|
|
136
|
+
? Results.Ok(user)
|
|
137
|
+
: Results.NotFound());
|
|
138
|
+
|
|
139
|
+
users.MapPost("/", async (CreateUserRequest request, IUserService service, CancellationToken ct) =>
|
|
140
|
+
{
|
|
141
|
+
var user = await service.CreateAsync(request, ct);
|
|
142
|
+
return Results.Created($"/api/v1/users/{user.Id}", user);
|
|
143
|
+
})
|
|
144
|
+
.RequireAuthorization();
|
|
145
|
+
|
|
146
|
+
// With validation
|
|
147
|
+
users.MapPost("/", async (
|
|
148
|
+
[FromBody] CreateUserRequest request,
|
|
149
|
+
IValidator<CreateUserRequest> validator,
|
|
150
|
+
IUserService service,
|
|
151
|
+
CancellationToken ct) =>
|
|
152
|
+
{
|
|
153
|
+
var validation = await validator.ValidateAsync(request, ct);
|
|
154
|
+
if (!validation.IsValid)
|
|
155
|
+
{
|
|
156
|
+
return Results.ValidationProblem(validation.ToDictionary());
|
|
157
|
+
}
|
|
158
|
+
var user = await service.CreateAsync(request, ct);
|
|
159
|
+
return Results.Created($"/api/v1/users/{user.Id}", user);
|
|
160
|
+
});
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
## Validation with FluentValidation
|
|
164
|
+
\`\`\`csharp
|
|
165
|
+
public record CreateUserRequest(string Email, string Name, string Password);
|
|
166
|
+
|
|
167
|
+
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
|
|
168
|
+
{
|
|
169
|
+
public CreateUserRequestValidator()
|
|
170
|
+
{
|
|
171
|
+
RuleFor(x => x.Email)
|
|
172
|
+
.NotEmpty()
|
|
173
|
+
.EmailAddress()
|
|
174
|
+
.MaximumLength(255);
|
|
175
|
+
|
|
176
|
+
RuleFor(x => x.Name)
|
|
177
|
+
.NotEmpty()
|
|
178
|
+
.MinimumLength(2)
|
|
179
|
+
.MaximumLength(100);
|
|
180
|
+
|
|
181
|
+
RuleFor(x => x.Password)
|
|
182
|
+
.NotEmpty()
|
|
183
|
+
.MinimumLength(8)
|
|
184
|
+
.Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
|
|
185
|
+
.Matches("[0-9]").WithMessage("Password must contain digit");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Registration
|
|
190
|
+
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
## Exception Handling
|
|
194
|
+
\`\`\`csharp
|
|
195
|
+
// Global exception handler
|
|
196
|
+
public class GlobalExceptionHandler : IExceptionHandler
|
|
197
|
+
{
|
|
198
|
+
private readonly ILogger<GlobalExceptionHandler> _logger;
|
|
199
|
+
|
|
200
|
+
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
|
|
201
|
+
{
|
|
202
|
+
_logger = logger;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public async ValueTask<bool> TryHandleAsync(
|
|
206
|
+
HttpContext context,
|
|
207
|
+
Exception exception,
|
|
208
|
+
CancellationToken ct)
|
|
209
|
+
{
|
|
210
|
+
_logger.LogError(exception, "Exception occurred: {Message}", exception.Message);
|
|
211
|
+
|
|
212
|
+
var (statusCode, response) = exception switch
|
|
213
|
+
{
|
|
214
|
+
NotFoundException e => (StatusCodes.Status404NotFound,
|
|
215
|
+
new ProblemDetails { Title = "Not Found", Detail = e.Message }),
|
|
216
|
+
ValidationException e => (StatusCodes.Status400BadRequest,
|
|
217
|
+
new ProblemDetails { Title = "Validation Error", Detail = e.Message }),
|
|
218
|
+
UnauthorizedAccessException => (StatusCodes.Status401Unauthorized,
|
|
219
|
+
new ProblemDetails { Title = "Unauthorized" }),
|
|
220
|
+
_ => (StatusCodes.Status500InternalServerError,
|
|
221
|
+
new ProblemDetails { Title = "Internal Server Error" })
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
context.Response.StatusCode = statusCode;
|
|
225
|
+
await context.Response.WriteAsJsonAsync(response, ct);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Registration
|
|
231
|
+
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
|
|
232
|
+
builder.Services.AddProblemDetails();
|
|
233
|
+
|
|
234
|
+
// In pipeline
|
|
235
|
+
app.UseExceptionHandler();
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
## Middleware
|
|
239
|
+
\`\`\`csharp
|
|
240
|
+
public class RequestTimingMiddleware
|
|
241
|
+
{
|
|
242
|
+
private readonly RequestDelegate _next;
|
|
243
|
+
private readonly ILogger<RequestTimingMiddleware> _logger;
|
|
244
|
+
|
|
245
|
+
public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
|
|
246
|
+
{
|
|
247
|
+
_next = next;
|
|
248
|
+
_logger = logger;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public async Task InvokeAsync(HttpContext context)
|
|
252
|
+
{
|
|
253
|
+
var stopwatch = Stopwatch.StartNew();
|
|
254
|
+
|
|
255
|
+
try
|
|
256
|
+
{
|
|
257
|
+
await _next(context);
|
|
258
|
+
}
|
|
259
|
+
finally
|
|
260
|
+
{
|
|
261
|
+
stopwatch.Stop();
|
|
262
|
+
_logger.LogInformation(
|
|
263
|
+
"{Method} {Path} responded {StatusCode} in {ElapsedMs}ms",
|
|
264
|
+
context.Request.Method,
|
|
265
|
+
context.Request.Path,
|
|
266
|
+
context.Response.StatusCode,
|
|
267
|
+
stopwatch.ElapsedMilliseconds);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Registration
|
|
273
|
+
app.UseMiddleware<RequestTimingMiddleware>();
|
|
274
|
+
|
|
275
|
+
// Or as extension method
|
|
276
|
+
public static class MiddlewareExtensions
|
|
277
|
+
{
|
|
278
|
+
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
|
|
279
|
+
=> app.UseMiddleware<RequestTimingMiddleware>();
|
|
280
|
+
}
|
|
281
|
+
\`\`\`
|
|
282
|
+
|
|
283
|
+
## Configuration
|
|
284
|
+
\`\`\`csharp
|
|
285
|
+
// appsettings.json
|
|
286
|
+
{
|
|
287
|
+
"ConnectionStrings": {
|
|
288
|
+
"Default": "Host=localhost;Database=myapp;Username=postgres;Password=secret"
|
|
289
|
+
},
|
|
290
|
+
"Jwt": {
|
|
291
|
+
"Key": "your-secret-key-here",
|
|
292
|
+
"Issuer": "your-app",
|
|
293
|
+
"Audience": "your-app"
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Strongly-typed configuration
|
|
298
|
+
public class JwtSettings
|
|
299
|
+
{
|
|
300
|
+
public required string Key { get; init; }
|
|
301
|
+
public required string Issuer { get; init; }
|
|
302
|
+
public required string Audience { get; init; }
|
|
303
|
+
public int ExpirationMinutes { get; init; } = 60;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Registration
|
|
307
|
+
builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("Jwt"));
|
|
308
|
+
|
|
309
|
+
// Usage
|
|
310
|
+
public class TokenService
|
|
311
|
+
{
|
|
312
|
+
private readonly JwtSettings _settings;
|
|
313
|
+
|
|
314
|
+
public TokenService(IOptions<JwtSettings> options)
|
|
315
|
+
{
|
|
316
|
+
_settings = options.Value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
\`\`\`
|
|
320
|
+
|
|
321
|
+
## Testing
|
|
322
|
+
\`\`\`csharp
|
|
323
|
+
public class UsersControllerTests : IClassFixture<WebApplicationFactory<Program>>
|
|
324
|
+
{
|
|
325
|
+
private readonly HttpClient _client;
|
|
326
|
+
|
|
327
|
+
public UsersControllerTests(WebApplicationFactory<Program> factory)
|
|
328
|
+
{
|
|
329
|
+
_client = factory.WithWebHostBuilder(builder =>
|
|
330
|
+
{
|
|
331
|
+
builder.ConfigureServices(services =>
|
|
332
|
+
{
|
|
333
|
+
// Replace with test database
|
|
334
|
+
services.RemoveAll<DbContextOptions<AppDbContext>>();
|
|
335
|
+
services.AddDbContext<AppDbContext>(options =>
|
|
336
|
+
options.UseInMemoryDatabase("TestDb"));
|
|
337
|
+
});
|
|
338
|
+
}).CreateClient();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
[Fact]
|
|
342
|
+
public async Task GetUser_ReturnsUser_WhenExists()
|
|
343
|
+
{
|
|
344
|
+
var response = await _client.GetAsync("/api/v1/users/1");
|
|
345
|
+
|
|
346
|
+
response.StatusCode.Should().Be(HttpStatusCode.OK);
|
|
347
|
+
var user = await response.Content.ReadFromJsonAsync<UserResponse>();
|
|
348
|
+
user.Should().NotBeNull();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
[Fact]
|
|
352
|
+
public async Task CreateUser_ReturnsCreated_WithValidData()
|
|
353
|
+
{
|
|
354
|
+
var request = new CreateUserRequest("test@example.com", "Test", "Password123");
|
|
355
|
+
|
|
356
|
+
var response = await _client.PostAsJsonAsync("/api/v1/users", request);
|
|
357
|
+
|
|
358
|
+
response.StatusCode.Should().Be(HttpStatusCode.Created);
|
|
359
|
+
response.Headers.Location.Should().NotBeNull();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
\`\`\`
|
|
363
|
+
|
|
364
|
+
## ✅ DO
|
|
365
|
+
- Use \`[ApiController]\` attribute for automatic model validation
|
|
366
|
+
- Use \`CancellationToken\` in all async endpoints
|
|
367
|
+
- Return \`ActionResult<T>\` for typed responses
|
|
368
|
+
- Use \`IOptions<T>\` for configuration
|
|
369
|
+
- Use global exception handling
|
|
370
|
+
- Use ProblemDetails for error responses
|
|
371
|
+
|
|
372
|
+
## ❌ DON'T
|
|
373
|
+
- Don't inject \`IConfiguration\` directly - use \`IOptions<T>\`
|
|
374
|
+
- Don't use \`[FromBody]\` on complex types with \`[ApiController]\`
|
|
375
|
+
- Don't forget \`ConfigureAwait(false)\` in library code
|
|
376
|
+
- Don't catch exceptions in controllers - use global handler
|
|
377
|
+
- Don't return entities - use DTOs/records
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Astro Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
src/
|
|
6
|
+
├── pages/ # File-based routing
|
|
7
|
+
│ ├── index.astro # /
|
|
8
|
+
│ ├── about.astro # /about
|
|
9
|
+
│ ├── posts/
|
|
10
|
+
│ │ ├── index.astro # /posts
|
|
11
|
+
│ │ └── [slug].astro # /posts/:slug
|
|
12
|
+
│ └── api/
|
|
13
|
+
│ └── users.ts # API endpoint
|
|
14
|
+
├── layouts/ # Page layouts
|
|
15
|
+
├── components/ # UI components
|
|
16
|
+
├── content/ # Content collections
|
|
17
|
+
│ └── blog/
|
|
18
|
+
│ ├── post-1.md
|
|
19
|
+
│ └── post-2.mdx
|
|
20
|
+
└── styles/ # Global styles
|
|
21
|
+
\`\`\`
|
|
22
|
+
|
|
23
|
+
## Astro Components
|
|
24
|
+
\`\`\`astro
|
|
25
|
+
---
|
|
26
|
+
// Component script (runs at build time)
|
|
27
|
+
import Layout from '../layouts/Layout.astro';
|
|
28
|
+
import Card from '../components/Card.astro';
|
|
29
|
+
|
|
30
|
+
// Props
|
|
31
|
+
interface Props {
|
|
32
|
+
title: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { title, description = 'Default description' } = Astro.props;
|
|
37
|
+
|
|
38
|
+
// Fetch data at build time
|
|
39
|
+
const response = await fetch('https://api.example.com/posts');
|
|
40
|
+
const posts = await response.json();
|
|
41
|
+
|
|
42
|
+
// Access URL params
|
|
43
|
+
const { slug } = Astro.params;
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<Layout title={title}>
|
|
47
|
+
<h1>{title}</h1>
|
|
48
|
+
<p>{description}</p>
|
|
49
|
+
|
|
50
|
+
<ul>
|
|
51
|
+
{posts.map((post) => (
|
|
52
|
+
<li>
|
|
53
|
+
<Card title={post.title} href={\`/posts/\${post.slug}\`} />
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
</Layout>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
/* Scoped styles by default */
|
|
61
|
+
h1 {
|
|
62
|
+
color: navy;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
## Component Islands (Partial Hydration)
|
|
68
|
+
\`\`\`astro
|
|
69
|
+
---
|
|
70
|
+
import ReactCounter from './Counter.tsx';
|
|
71
|
+
import VueWidget from './Widget.vue';
|
|
72
|
+
import SvelteForm from './Form.svelte';
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
<!-- No JS - static HTML only -->
|
|
76
|
+
<ReactCounter />
|
|
77
|
+
|
|
78
|
+
<!-- Hydrate immediately on page load -->
|
|
79
|
+
<ReactCounter client:load />
|
|
80
|
+
|
|
81
|
+
<!-- Hydrate when component is visible -->
|
|
82
|
+
<ReactCounter client:visible />
|
|
83
|
+
|
|
84
|
+
<!-- Hydrate on idle (requestIdleCallback) -->
|
|
85
|
+
<ReactCounter client:idle />
|
|
86
|
+
|
|
87
|
+
<!-- Hydrate on media query match -->
|
|
88
|
+
<VueWidget client:media="(max-width: 768px)" />
|
|
89
|
+
|
|
90
|
+
<!-- Hydrate only on client (skip SSR) -->
|
|
91
|
+
<SvelteForm client:only="svelte" />
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
## Content Collections
|
|
95
|
+
\`\`\`typescript
|
|
96
|
+
// src/content/config.ts
|
|
97
|
+
import { defineCollection, z } from 'astro:content';
|
|
98
|
+
|
|
99
|
+
const blogCollection = defineCollection({
|
|
100
|
+
type: 'content',
|
|
101
|
+
schema: z.object({
|
|
102
|
+
title: z.string(),
|
|
103
|
+
description: z.string(),
|
|
104
|
+
pubDate: z.date(),
|
|
105
|
+
author: z.string(),
|
|
106
|
+
tags: z.array(z.string()).optional(),
|
|
107
|
+
image: z.string().optional(),
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const collections = {
|
|
112
|
+
blog: blogCollection,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Usage in pages
|
|
116
|
+
---
|
|
117
|
+
import { getCollection, getEntry } from 'astro:content';
|
|
118
|
+
|
|
119
|
+
// Get all posts
|
|
120
|
+
const posts = await getCollection('blog');
|
|
121
|
+
|
|
122
|
+
// Filter posts
|
|
123
|
+
const publishedPosts = await getCollection('blog', ({ data }) => {
|
|
124
|
+
return data.pubDate < new Date();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Get single entry
|
|
128
|
+
const post = await getEntry('blog', 'my-post-slug');
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
{posts.map((post) => (
|
|
132
|
+
<article>
|
|
133
|
+
<h2>{post.data.title}</h2>
|
|
134
|
+
<time>{post.data.pubDate.toDateString()}</time>
|
|
135
|
+
</article>
|
|
136
|
+
))}
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
## Dynamic Routes
|
|
140
|
+
\`\`\`astro
|
|
141
|
+
---
|
|
142
|
+
// src/pages/posts/[slug].astro
|
|
143
|
+
import { getCollection } from 'astro:content';
|
|
144
|
+
|
|
145
|
+
export async function getStaticPaths() {
|
|
146
|
+
const posts = await getCollection('blog');
|
|
147
|
+
return posts.map((post) => ({
|
|
148
|
+
params: { slug: post.slug },
|
|
149
|
+
props: { post },
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { post } = Astro.props;
|
|
154
|
+
const { Content } = await post.render();
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
<article>
|
|
158
|
+
<h1>{post.data.title}</h1>
|
|
159
|
+
<Content />
|
|
160
|
+
</article>
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
## API Routes
|
|
164
|
+
\`\`\`typescript
|
|
165
|
+
// src/pages/api/users.ts
|
|
166
|
+
import type { APIRoute } from 'astro';
|
|
167
|
+
|
|
168
|
+
export const GET: APIRoute = async ({ request }) => {
|
|
169
|
+
const users = await db.user.findMany();
|
|
170
|
+
|
|
171
|
+
return new Response(JSON.stringify(users), {
|
|
172
|
+
status: 200,
|
|
173
|
+
headers: { 'Content-Type': 'application/json' },
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
178
|
+
const data = await request.json();
|
|
179
|
+
|
|
180
|
+
const user = await db.user.create({ data });
|
|
181
|
+
|
|
182
|
+
return new Response(JSON.stringify(user), {
|
|
183
|
+
status: 201,
|
|
184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
## Middleware
|
|
190
|
+
\`\`\`typescript
|
|
191
|
+
// src/middleware.ts
|
|
192
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
193
|
+
|
|
194
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
195
|
+
// Run before each page/endpoint
|
|
196
|
+
const token = context.cookies.get('token');
|
|
197
|
+
|
|
198
|
+
if (token) {
|
|
199
|
+
context.locals.user = await validateToken(token.value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Protected routes
|
|
203
|
+
if (context.url.pathname.startsWith('/dashboard') && !context.locals.user) {
|
|
204
|
+
return context.redirect('/login');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return next();
|
|
208
|
+
});
|
|
209
|
+
\`\`\`
|
|
210
|
+
|
|
211
|
+
## SSR Mode (Hybrid/Server)
|
|
212
|
+
\`\`\`typescript
|
|
213
|
+
// astro.config.mjs
|
|
214
|
+
export default defineConfig({
|
|
215
|
+
output: 'hybrid', // or 'server' for full SSR
|
|
216
|
+
|
|
217
|
+
adapter: vercel(), // or netlify(), node(), etc.
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Per-page opt-in/out
|
|
221
|
+
---
|
|
222
|
+
// Static page in hybrid mode
|
|
223
|
+
export const prerender = true;
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
// Dynamic page (SSR)
|
|
228
|
+
export const prerender = false;
|
|
229
|
+
const user = Astro.locals.user;
|
|
230
|
+
---
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
## ❌ DON'T
|
|
234
|
+
- Hydrate everything (defeats the purpose)
|
|
235
|
+
- Use client:load when client:visible works
|
|
236
|
+
- Forget to define content collection schemas
|
|
237
|
+
- Skip prerender in hybrid mode for static pages
|
|
238
|
+
|
|
239
|
+
## ✅ DO
|
|
240
|
+
- Default to zero JavaScript
|
|
241
|
+
- Hydrate only interactive components
|
|
242
|
+
- Use content collections for structured content
|
|
243
|
+
- Use middleware for auth/redirects
|
|
244
|
+
- Choose the right hydration directive
|
|
245
|
+
- Use hybrid mode for mixed static/dynamic
|