@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.
Files changed (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /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
+ ```