@malamute/ai-rules 1.0.0 → 1.2.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/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.cs"
|
|
4
|
+
- "src/**/Program.cs"
|
|
5
|
+
- "src/**/Startup.cs"
|
|
6
|
+
- "src/**/DependencyInjection.cs"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Dependency Injection (.NET)
|
|
10
|
+
|
|
11
|
+
## Service Registration
|
|
12
|
+
|
|
13
|
+
```csharp
|
|
14
|
+
// Application/DependencyInjection.cs
|
|
15
|
+
public static class DependencyInjection
|
|
16
|
+
{
|
|
17
|
+
public static IServiceCollection AddApplication(this IServiceCollection services)
|
|
18
|
+
{
|
|
19
|
+
// MediatR
|
|
20
|
+
services.AddMediatR(cfg =>
|
|
21
|
+
{
|
|
22
|
+
cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
|
|
23
|
+
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
|
24
|
+
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// FluentValidation
|
|
28
|
+
services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly);
|
|
29
|
+
|
|
30
|
+
// Application services
|
|
31
|
+
services.AddScoped<IOrderPricingService, OrderPricingService>();
|
|
32
|
+
|
|
33
|
+
return services;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Infrastructure/DependencyInjection.cs
|
|
38
|
+
public static class DependencyInjection
|
|
39
|
+
{
|
|
40
|
+
public static IServiceCollection AddInfrastructure(
|
|
41
|
+
this IServiceCollection services,
|
|
42
|
+
IConfiguration configuration)
|
|
43
|
+
{
|
|
44
|
+
// Database
|
|
45
|
+
services.AddDbContext<ApplicationDbContext>(options =>
|
|
46
|
+
options.UseNpgsql(configuration.GetConnectionString("Database")));
|
|
47
|
+
|
|
48
|
+
// Repositories
|
|
49
|
+
services.AddScoped<IUserRepository, UserRepository>();
|
|
50
|
+
services.AddScoped<IOrderRepository, OrderRepository>();
|
|
51
|
+
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
|
52
|
+
|
|
53
|
+
// External services
|
|
54
|
+
services.AddScoped<IEmailService, SendGridEmailService>();
|
|
55
|
+
services.AddHttpClient<IPaymentGateway, StripePaymentGateway>();
|
|
56
|
+
|
|
57
|
+
// Caching
|
|
58
|
+
services.AddStackExchangeRedisCache(options =>
|
|
59
|
+
options.Configuration = configuration.GetConnectionString("Redis"));
|
|
60
|
+
|
|
61
|
+
return services;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Program.cs
|
|
66
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
67
|
+
|
|
68
|
+
builder.Services
|
|
69
|
+
.AddApplication()
|
|
70
|
+
.AddInfrastructure(builder.Configuration);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Primary Constructor Injection (C# 12)
|
|
74
|
+
|
|
75
|
+
```csharp
|
|
76
|
+
// Preferred: Primary constructor
|
|
77
|
+
public class UserService(
|
|
78
|
+
IUserRepository userRepository,
|
|
79
|
+
IPasswordHasher passwordHasher,
|
|
80
|
+
ILogger<UserService> logger)
|
|
81
|
+
{
|
|
82
|
+
public async Task<Result<User>> CreateAsync(CreateUserCommand command, CancellationToken ct)
|
|
83
|
+
{
|
|
84
|
+
logger.LogInformation("Creating user {Email}", command.Email);
|
|
85
|
+
// Use injected dependencies directly
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Also valid: Constructor injection
|
|
90
|
+
public class UserService
|
|
91
|
+
{
|
|
92
|
+
private readonly IUserRepository _userRepository;
|
|
93
|
+
private readonly IPasswordHasher _passwordHasher;
|
|
94
|
+
private readonly ILogger<UserService> _logger;
|
|
95
|
+
|
|
96
|
+
public UserService(
|
|
97
|
+
IUserRepository userRepository,
|
|
98
|
+
IPasswordHasher passwordHasher,
|
|
99
|
+
ILogger<UserService> logger)
|
|
100
|
+
{
|
|
101
|
+
_userRepository = userRepository;
|
|
102
|
+
_passwordHasher = passwordHasher;
|
|
103
|
+
_logger = logger;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Lifetimes
|
|
109
|
+
|
|
110
|
+
```csharp
|
|
111
|
+
// Singleton - One instance for the entire app
|
|
112
|
+
// Use for: Stateless services, caches, configuration
|
|
113
|
+
services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
|
|
114
|
+
services.AddSingleton<ICacheService, RedisCacheService>();
|
|
115
|
+
|
|
116
|
+
// Scoped - One instance per request
|
|
117
|
+
// Use for: DbContext, repositories, unit of work, user context
|
|
118
|
+
services.AddScoped<ApplicationDbContext>();
|
|
119
|
+
services.AddScoped<IUserRepository, UserRepository>();
|
|
120
|
+
services.AddScoped<ICurrentUserService, CurrentUserService>();
|
|
121
|
+
|
|
122
|
+
// Transient - New instance every time
|
|
123
|
+
// Use for: Lightweight, stateless services
|
|
124
|
+
services.AddTransient<IEmailBuilder, EmailBuilder>();
|
|
125
|
+
services.AddTransient<IPdfGenerator, PdfGenerator>();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Options Pattern
|
|
129
|
+
|
|
130
|
+
```csharp
|
|
131
|
+
// Settings class
|
|
132
|
+
public class JwtSettings
|
|
133
|
+
{
|
|
134
|
+
public const string SectionName = "Jwt";
|
|
135
|
+
|
|
136
|
+
public required string Secret { get; init; }
|
|
137
|
+
public required string Issuer { get; init; }
|
|
138
|
+
public required string Audience { get; init; }
|
|
139
|
+
public int ExpiryMinutes { get; init; } = 60;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Registration
|
|
143
|
+
services.Configure<JwtSettings>(configuration.GetSection(JwtSettings.SectionName));
|
|
144
|
+
|
|
145
|
+
// Or with validation
|
|
146
|
+
services.AddOptions<JwtSettings>()
|
|
147
|
+
.Bind(configuration.GetSection(JwtSettings.SectionName))
|
|
148
|
+
.ValidateDataAnnotations()
|
|
149
|
+
.ValidateOnStart();
|
|
150
|
+
|
|
151
|
+
// Usage - IOptions (singleton-like, read once at startup)
|
|
152
|
+
public class JwtService(IOptions<JwtSettings> options)
|
|
153
|
+
{
|
|
154
|
+
private readonly JwtSettings _settings = options.Value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Usage - IOptionsSnapshot (scoped, can reload)
|
|
158
|
+
public class JwtService(IOptionsSnapshot<JwtSettings> options)
|
|
159
|
+
{
|
|
160
|
+
private readonly JwtSettings _settings = options.Value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Usage - IOptionsMonitor (singleton, live updates)
|
|
164
|
+
public class JwtService(IOptionsMonitor<JwtSettings> options)
|
|
165
|
+
{
|
|
166
|
+
public void DoSomething()
|
|
167
|
+
{
|
|
168
|
+
var settings = options.CurrentValue; // Always current
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Keyed Services (.NET 8+)
|
|
174
|
+
|
|
175
|
+
```csharp
|
|
176
|
+
// Registration
|
|
177
|
+
services.AddKeyedScoped<IPaymentGateway, StripeGateway>("stripe");
|
|
178
|
+
services.AddKeyedScoped<IPaymentGateway, PayPalGateway>("paypal");
|
|
179
|
+
services.AddKeyedScoped<IPaymentGateway, BraintreeGateway>("braintree");
|
|
180
|
+
|
|
181
|
+
// Injection
|
|
182
|
+
public class PaymentService([FromKeyedServices("stripe")] IPaymentGateway gateway)
|
|
183
|
+
{
|
|
184
|
+
public async Task ProcessAsync(Payment payment)
|
|
185
|
+
{
|
|
186
|
+
await gateway.ChargeAsync(payment);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Dynamic resolution
|
|
191
|
+
public class PaymentService(IServiceProvider serviceProvider)
|
|
192
|
+
{
|
|
193
|
+
public async Task ProcessAsync(Payment payment)
|
|
194
|
+
{
|
|
195
|
+
var gateway = serviceProvider.GetRequiredKeyedService<IPaymentGateway>(
|
|
196
|
+
payment.GatewayType);
|
|
197
|
+
await gateway.ChargeAsync(payment);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Factory Pattern
|
|
203
|
+
|
|
204
|
+
```csharp
|
|
205
|
+
// When you need runtime parameters
|
|
206
|
+
public interface IReportGeneratorFactory
|
|
207
|
+
{
|
|
208
|
+
IReportGenerator Create(ReportType type);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public class ReportGeneratorFactory(IServiceProvider serviceProvider) : IReportGeneratorFactory
|
|
212
|
+
{
|
|
213
|
+
public IReportGenerator Create(ReportType type) => type switch
|
|
214
|
+
{
|
|
215
|
+
ReportType.Pdf => serviceProvider.GetRequiredService<PdfReportGenerator>(),
|
|
216
|
+
ReportType.Excel => serviceProvider.GetRequiredService<ExcelReportGenerator>(),
|
|
217
|
+
ReportType.Csv => serviceProvider.GetRequiredService<CsvReportGenerator>(),
|
|
218
|
+
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Registration
|
|
223
|
+
services.AddScoped<IReportGeneratorFactory, ReportGeneratorFactory>();
|
|
224
|
+
services.AddScoped<PdfReportGenerator>();
|
|
225
|
+
services.AddScoped<ExcelReportGenerator>();
|
|
226
|
+
services.AddScoped<CsvReportGenerator>();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Decorator Pattern
|
|
230
|
+
|
|
231
|
+
```csharp
|
|
232
|
+
// Interface
|
|
233
|
+
public interface IUserRepository
|
|
234
|
+
{
|
|
235
|
+
Task<User?> GetByIdAsync(Guid id, CancellationToken ct);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Base implementation
|
|
239
|
+
public class UserRepository(ApplicationDbContext context) : IUserRepository
|
|
240
|
+
{
|
|
241
|
+
public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct) =>
|
|
242
|
+
await context.Users.FindAsync([id], ct);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Caching decorator
|
|
246
|
+
public class CachedUserRepository(
|
|
247
|
+
IUserRepository inner,
|
|
248
|
+
IDistributedCache cache) : IUserRepository
|
|
249
|
+
{
|
|
250
|
+
public async Task<User?> GetByIdAsync(Guid id, CancellationToken ct)
|
|
251
|
+
{
|
|
252
|
+
var cacheKey = $"user:{id}";
|
|
253
|
+
var cached = await cache.GetStringAsync(cacheKey, ct);
|
|
254
|
+
|
|
255
|
+
if (cached is not null)
|
|
256
|
+
{
|
|
257
|
+
return JsonSerializer.Deserialize<User>(cached);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
var user = await inner.GetByIdAsync(id, ct);
|
|
261
|
+
|
|
262
|
+
if (user is not null)
|
|
263
|
+
{
|
|
264
|
+
await cache.SetStringAsync(
|
|
265
|
+
cacheKey,
|
|
266
|
+
JsonSerializer.Serialize(user),
|
|
267
|
+
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) },
|
|
268
|
+
ct);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return user;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Registration with Scrutor
|
|
276
|
+
services.AddScoped<UserRepository>();
|
|
277
|
+
services.AddScoped<IUserRepository, UserRepository>();
|
|
278
|
+
services.Decorate<IUserRepository, CachedUserRepository>();
|
|
279
|
+
|
|
280
|
+
// Or manual decoration
|
|
281
|
+
services.AddScoped<UserRepository>();
|
|
282
|
+
services.AddScoped<IUserRepository>(sp =>
|
|
283
|
+
new CachedUserRepository(
|
|
284
|
+
sp.GetRequiredService<UserRepository>(),
|
|
285
|
+
sp.GetRequiredService<IDistributedCache>()));
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Anti-Patterns
|
|
289
|
+
|
|
290
|
+
```csharp
|
|
291
|
+
// BAD: Service locator pattern
|
|
292
|
+
public class UserService(IServiceProvider serviceProvider)
|
|
293
|
+
{
|
|
294
|
+
public async Task DoSomething()
|
|
295
|
+
{
|
|
296
|
+
var repo = serviceProvider.GetRequiredService<IUserRepository>();
|
|
297
|
+
// Hidden dependency!
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// GOOD: Explicit dependencies
|
|
302
|
+
public class UserService(IUserRepository userRepository)
|
|
303
|
+
{
|
|
304
|
+
public async Task DoSomething()
|
|
305
|
+
{
|
|
306
|
+
// Dependency is visible in constructor
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
// BAD: Captive dependency (scoped inside singleton)
|
|
312
|
+
services.AddSingleton<MySingletonService>(); // Singleton
|
|
313
|
+
services.AddScoped<ApplicationDbContext>(); // Scoped
|
|
314
|
+
|
|
315
|
+
public class MySingletonService(ApplicationDbContext context) // DbContext captured!
|
|
316
|
+
{
|
|
317
|
+
// This DbContext will live forever, causing memory leaks
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// GOOD: Use IServiceScopeFactory for scoped dependencies in singletons
|
|
321
|
+
public class MySingletonService(IServiceScopeFactory scopeFactory)
|
|
322
|
+
{
|
|
323
|
+
public async Task DoSomething()
|
|
324
|
+
{
|
|
325
|
+
using var scope = scopeFactory.CreateScope();
|
|
326
|
+
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
327
|
+
// Properly scoped
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
// BAD: New-ing up dependencies
|
|
333
|
+
public class UserService
|
|
334
|
+
{
|
|
335
|
+
private readonly UserRepository _repo = new UserRepository(); // Hard to test!
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// GOOD: Inject dependencies
|
|
339
|
+
public class UserService(IUserRepository repo)
|
|
340
|
+
{
|
|
341
|
+
// Easy to mock in tests
|
|
342
|
+
}
|
|
343
|
+
```
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.cs"
|
|
4
|
+
- "src/Application/**/*.cs"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MediatR Patterns
|
|
8
|
+
|
|
9
|
+
## Command/Query Separation
|
|
10
|
+
|
|
11
|
+
```csharp
|
|
12
|
+
// Commands - modify state, return minimal data
|
|
13
|
+
public record CreateUserCommand(string Email, string Name) : IRequest<Guid>;
|
|
14
|
+
public record UpdateUserCommand(Guid Id, string Name) : IRequest;
|
|
15
|
+
public record DeleteUserCommand(Guid Id) : IRequest;
|
|
16
|
+
|
|
17
|
+
// Queries - read data, never modify state
|
|
18
|
+
public record GetUserQuery(Guid Id) : IRequest<UserDto?>;
|
|
19
|
+
public record GetUsersQuery(int Page, int PageSize) : IRequest<PaginatedList<UserDto>>;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Command Handler
|
|
23
|
+
|
|
24
|
+
```csharp
|
|
25
|
+
public class CreateUserCommandHandler(
|
|
26
|
+
IUserRepository userRepository,
|
|
27
|
+
IPasswordHasher passwordHasher,
|
|
28
|
+
IPublisher publisher
|
|
29
|
+
) : IRequestHandler<CreateUserCommand, Guid>
|
|
30
|
+
{
|
|
31
|
+
public async Task<Guid> Handle(
|
|
32
|
+
CreateUserCommand request,
|
|
33
|
+
CancellationToken cancellationToken)
|
|
34
|
+
{
|
|
35
|
+
var user = User.Create(
|
|
36
|
+
request.Email,
|
|
37
|
+
request.Name,
|
|
38
|
+
passwordHasher.Hash(request.Password)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
await userRepository.AddAsync(user, cancellationToken);
|
|
42
|
+
|
|
43
|
+
// Publish domain event
|
|
44
|
+
await publisher.Publish(
|
|
45
|
+
new UserCreatedEvent(user.Id, user.Email),
|
|
46
|
+
cancellationToken
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return user.Id;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Query Handler
|
|
55
|
+
|
|
56
|
+
```csharp
|
|
57
|
+
public class GetUserQueryHandler(
|
|
58
|
+
IApplicationDbContext context
|
|
59
|
+
) : IRequestHandler<GetUserQuery, UserDto?>
|
|
60
|
+
{
|
|
61
|
+
public async Task<UserDto?> Handle(
|
|
62
|
+
GetUserQuery request,
|
|
63
|
+
CancellationToken cancellationToken)
|
|
64
|
+
{
|
|
65
|
+
return await context.Users
|
|
66
|
+
.Where(u => u.Id == request.Id)
|
|
67
|
+
.Select(u => new UserDto(u.Id, u.Email, u.Name, u.CreatedAt))
|
|
68
|
+
.FirstOrDefaultAsync(cancellationToken);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Validation Behavior (Pipeline)
|
|
74
|
+
|
|
75
|
+
```csharp
|
|
76
|
+
public class ValidationBehavior<TRequest, TResponse>(
|
|
77
|
+
IEnumerable<IValidator<TRequest>> validators
|
|
78
|
+
) : IPipelineBehavior<TRequest, TResponse>
|
|
79
|
+
where TRequest : notnull
|
|
80
|
+
{
|
|
81
|
+
public async Task<TResponse> Handle(
|
|
82
|
+
TRequest request,
|
|
83
|
+
RequestHandlerDelegate<TResponse> next,
|
|
84
|
+
CancellationToken cancellationToken)
|
|
85
|
+
{
|
|
86
|
+
if (!validators.Any())
|
|
87
|
+
{
|
|
88
|
+
return await next();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
var context = new ValidationContext<TRequest>(request);
|
|
92
|
+
|
|
93
|
+
var validationResults = await Task.WhenAll(
|
|
94
|
+
validators.Select(v => v.ValidateAsync(context, cancellationToken))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
var failures = validationResults
|
|
98
|
+
.SelectMany(r => r.Errors)
|
|
99
|
+
.Where(f => f != null)
|
|
100
|
+
.ToList();
|
|
101
|
+
|
|
102
|
+
if (failures.Count != 0)
|
|
103
|
+
{
|
|
104
|
+
throw new ValidationException(failures);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return await next();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Logging Behavior
|
|
113
|
+
|
|
114
|
+
```csharp
|
|
115
|
+
public class LoggingBehavior<TRequest, TResponse>(
|
|
116
|
+
ILogger<LoggingBehavior<TRequest, TResponse>> logger
|
|
117
|
+
) : IPipelineBehavior<TRequest, TResponse>
|
|
118
|
+
where TRequest : notnull
|
|
119
|
+
{
|
|
120
|
+
public async Task<TResponse> Handle(
|
|
121
|
+
TRequest request,
|
|
122
|
+
RequestHandlerDelegate<TResponse> next,
|
|
123
|
+
CancellationToken cancellationToken)
|
|
124
|
+
{
|
|
125
|
+
var requestName = typeof(TRequest).Name;
|
|
126
|
+
|
|
127
|
+
logger.LogInformation(
|
|
128
|
+
"Handling {RequestName} {@Request}",
|
|
129
|
+
requestName,
|
|
130
|
+
request
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
var stopwatch = Stopwatch.StartNew();
|
|
134
|
+
|
|
135
|
+
try
|
|
136
|
+
{
|
|
137
|
+
var response = await next();
|
|
138
|
+
|
|
139
|
+
stopwatch.Stop();
|
|
140
|
+
|
|
141
|
+
logger.LogInformation(
|
|
142
|
+
"Handled {RequestName} in {ElapsedMilliseconds}ms",
|
|
143
|
+
requestName,
|
|
144
|
+
stopwatch.ElapsedMilliseconds
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return response;
|
|
148
|
+
}
|
|
149
|
+
catch (Exception ex)
|
|
150
|
+
{
|
|
151
|
+
stopwatch.Stop();
|
|
152
|
+
|
|
153
|
+
logger.LogError(
|
|
154
|
+
ex,
|
|
155
|
+
"Error handling {RequestName} after {ElapsedMilliseconds}ms",
|
|
156
|
+
requestName,
|
|
157
|
+
stopwatch.ElapsedMilliseconds
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
throw;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Transaction Behavior
|
|
167
|
+
|
|
168
|
+
```csharp
|
|
169
|
+
public class TransactionBehavior<TRequest, TResponse>(
|
|
170
|
+
IApplicationDbContext context,
|
|
171
|
+
ILogger<TransactionBehavior<TRequest, TResponse>> logger
|
|
172
|
+
) : IPipelineBehavior<TRequest, TResponse>
|
|
173
|
+
where TRequest : notnull
|
|
174
|
+
{
|
|
175
|
+
public async Task<TResponse> Handle(
|
|
176
|
+
TRequest request,
|
|
177
|
+
RequestHandlerDelegate<TResponse> next,
|
|
178
|
+
CancellationToken cancellationToken)
|
|
179
|
+
{
|
|
180
|
+
// Only wrap commands (not queries) in transaction
|
|
181
|
+
if (!typeof(TRequest).Name.EndsWith("Command"))
|
|
182
|
+
{
|
|
183
|
+
return await next();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await using var transaction = await context.Database
|
|
187
|
+
.BeginTransactionAsync(cancellationToken);
|
|
188
|
+
|
|
189
|
+
try
|
|
190
|
+
{
|
|
191
|
+
var response = await next();
|
|
192
|
+
await transaction.CommitAsync(cancellationToken);
|
|
193
|
+
return response;
|
|
194
|
+
}
|
|
195
|
+
catch
|
|
196
|
+
{
|
|
197
|
+
await transaction.RollbackAsync(cancellationToken);
|
|
198
|
+
throw;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## FluentValidation Validators
|
|
205
|
+
|
|
206
|
+
```csharp
|
|
207
|
+
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
|
|
208
|
+
{
|
|
209
|
+
public CreateUserCommandValidator(IUserRepository userRepository)
|
|
210
|
+
{
|
|
211
|
+
RuleFor(x => x.Email)
|
|
212
|
+
.NotEmpty()
|
|
213
|
+
.EmailAddress()
|
|
214
|
+
.MustAsync(async (email, ct) => !await userRepository.ExistsAsync(email, ct))
|
|
215
|
+
.WithMessage("Email already exists");
|
|
216
|
+
|
|
217
|
+
RuleFor(x => x.Name)
|
|
218
|
+
.NotEmpty()
|
|
219
|
+
.MaximumLength(100);
|
|
220
|
+
|
|
221
|
+
RuleFor(x => x.Password)
|
|
222
|
+
.NotEmpty()
|
|
223
|
+
.MinimumLength(8)
|
|
224
|
+
.Matches("[A-Z]").WithMessage("Must contain uppercase letter")
|
|
225
|
+
.Matches("[a-z]").WithMessage("Must contain lowercase letter")
|
|
226
|
+
.Matches("[0-9]").WithMessage("Must contain digit");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Domain Events (Notifications)
|
|
232
|
+
|
|
233
|
+
```csharp
|
|
234
|
+
// Event
|
|
235
|
+
public record UserCreatedEvent(Guid UserId, string Email) : INotification;
|
|
236
|
+
|
|
237
|
+
// Multiple handlers for same event
|
|
238
|
+
public class SendWelcomeEmailHandler(IEmailService emailService)
|
|
239
|
+
: INotificationHandler<UserCreatedEvent>
|
|
240
|
+
{
|
|
241
|
+
public async Task Handle(
|
|
242
|
+
UserCreatedEvent notification,
|
|
243
|
+
CancellationToken cancellationToken)
|
|
244
|
+
{
|
|
245
|
+
await emailService.SendWelcomeEmailAsync(
|
|
246
|
+
notification.Email,
|
|
247
|
+
cancellationToken
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
public class CreateUserAnalyticsHandler(IAnalyticsService analytics)
|
|
253
|
+
: INotificationHandler<UserCreatedEvent>
|
|
254
|
+
{
|
|
255
|
+
public async Task Handle(
|
|
256
|
+
UserCreatedEvent notification,
|
|
257
|
+
CancellationToken cancellationToken)
|
|
258
|
+
{
|
|
259
|
+
await analytics.TrackAsync(
|
|
260
|
+
"user_created",
|
|
261
|
+
new { notification.UserId },
|
|
262
|
+
cancellationToken
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Registration (Program.cs)
|
|
269
|
+
|
|
270
|
+
```csharp
|
|
271
|
+
builder.Services.AddMediatR(cfg =>
|
|
272
|
+
{
|
|
273
|
+
cfg.RegisterServicesFromAssemblyContaining<CreateUserCommand>();
|
|
274
|
+
|
|
275
|
+
// Pipeline behaviors (order matters)
|
|
276
|
+
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
|
|
277
|
+
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
|
|
278
|
+
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Register FluentValidation validators
|
|
282
|
+
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserCommandValidator>();
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Usage in Controller/Endpoint
|
|
286
|
+
|
|
287
|
+
```csharp
|
|
288
|
+
// Minimal API
|
|
289
|
+
app.MapPost("/api/users", async (CreateUserCommand command, ISender sender) =>
|
|
290
|
+
{
|
|
291
|
+
var userId = await sender.Send(command);
|
|
292
|
+
return Results.CreatedAtRoute("GetUser", new { id = userId }, new { id = userId });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
app.MapGet("/api/users/{id:guid}", async (Guid id, ISender sender) =>
|
|
296
|
+
{
|
|
297
|
+
var user = await sender.Send(new GetUserQuery(id));
|
|
298
|
+
return user is not null ? Results.Ok(user) : Results.NotFound();
|
|
299
|
+
}).WithName("GetUser");
|
|
300
|
+
|
|
301
|
+
// Controller
|
|
302
|
+
[ApiController]
|
|
303
|
+
[Route("api/[controller]")]
|
|
304
|
+
public class UsersController(ISender sender) : ControllerBase
|
|
305
|
+
{
|
|
306
|
+
[HttpPost]
|
|
307
|
+
public async Task<IActionResult> Create(CreateUserCommand command)
|
|
308
|
+
{
|
|
309
|
+
var userId = await sender.Send(command);
|
|
310
|
+
return CreatedAtAction(nameof(Get), new { id = userId }, new { id = userId });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
[HttpGet("{id:guid}")]
|
|
314
|
+
public async Task<IActionResult> Get(Guid id)
|
|
315
|
+
{
|
|
316
|
+
var user = await sender.Send(new GetUserQuery(id));
|
|
317
|
+
return user is not null ? Ok(user) : NotFound();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|