@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,363 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.cs"
|
|
4
|
+
- "src/Application/**/*.cs"
|
|
5
|
+
- "src/Domain/**/*.cs"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Result Pattern
|
|
9
|
+
|
|
10
|
+
## Result Type
|
|
11
|
+
|
|
12
|
+
```csharp
|
|
13
|
+
// Domain/Common/Result.cs
|
|
14
|
+
public class Result
|
|
15
|
+
{
|
|
16
|
+
protected Result(bool isSuccess, Error error)
|
|
17
|
+
{
|
|
18
|
+
if (isSuccess && error != Error.None ||
|
|
19
|
+
!isSuccess && error == Error.None)
|
|
20
|
+
{
|
|
21
|
+
throw new ArgumentException("Invalid error state", nameof(error));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
IsSuccess = isSuccess;
|
|
25
|
+
Error = error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public bool IsSuccess { get; }
|
|
29
|
+
public bool IsFailure => !IsSuccess;
|
|
30
|
+
public Error Error { get; }
|
|
31
|
+
|
|
32
|
+
public static Result Success() => new(true, Error.None);
|
|
33
|
+
public static Result Failure(Error error) => new(false, error);
|
|
34
|
+
|
|
35
|
+
public static Result<T> Success<T>(T value) => new(value, true, Error.None);
|
|
36
|
+
public static Result<T> Failure<T>(Error error) => new(default!, false, error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public class Result<T> : Result
|
|
40
|
+
{
|
|
41
|
+
private readonly T _value;
|
|
42
|
+
|
|
43
|
+
protected internal Result(T value, bool isSuccess, Error error)
|
|
44
|
+
: base(isSuccess, error)
|
|
45
|
+
{
|
|
46
|
+
_value = value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public T Value => IsSuccess
|
|
50
|
+
? _value
|
|
51
|
+
: throw new InvalidOperationException("Cannot access value of failed result");
|
|
52
|
+
|
|
53
|
+
public static implicit operator Result<T>(T value) => Success(value);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Error Type
|
|
58
|
+
|
|
59
|
+
```csharp
|
|
60
|
+
// Domain/Common/Error.cs
|
|
61
|
+
public sealed record Error(string Code, string Description)
|
|
62
|
+
{
|
|
63
|
+
public static readonly Error None = new(string.Empty, string.Empty);
|
|
64
|
+
public static readonly Error NullValue = new("Error.NullValue", "A null value was provided");
|
|
65
|
+
|
|
66
|
+
public static Error NotFound(string entityName, object id) =>
|
|
67
|
+
new($"{entityName}.NotFound", $"{entityName} with id {id} was not found");
|
|
68
|
+
|
|
69
|
+
public static Error Validation(string propertyName, string message) =>
|
|
70
|
+
new($"Validation.{propertyName}", message);
|
|
71
|
+
|
|
72
|
+
public static Error Conflict(string entityName, string details) =>
|
|
73
|
+
new($"{entityName}.Conflict", details);
|
|
74
|
+
|
|
75
|
+
public static Error Unauthorized(string message = "Unauthorized access") =>
|
|
76
|
+
new("Auth.Unauthorized", message);
|
|
77
|
+
|
|
78
|
+
public static Error Forbidden(string message = "Access denied") =>
|
|
79
|
+
new("Auth.Forbidden", message);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Domain-specific errors
|
|
83
|
+
public static class UserErrors
|
|
84
|
+
{
|
|
85
|
+
public static Error NotFound(Guid id) =>
|
|
86
|
+
Error.NotFound("User", id);
|
|
87
|
+
|
|
88
|
+
public static Error EmailAlreadyExists(string email) =>
|
|
89
|
+
Error.Conflict("User", $"Email {email} is already registered");
|
|
90
|
+
|
|
91
|
+
public static Error InvalidPassword =>
|
|
92
|
+
Error.Validation("Password", "Password does not meet requirements");
|
|
93
|
+
|
|
94
|
+
public static Error AccountLocked =>
|
|
95
|
+
new("User.AccountLocked", "Account has been locked due to too many failed attempts");
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Usage in Handlers
|
|
100
|
+
|
|
101
|
+
```csharp
|
|
102
|
+
// Application/Users/Commands/CreateUserCommandHandler.cs
|
|
103
|
+
public class CreateUserCommandHandler(
|
|
104
|
+
IUserRepository userRepository,
|
|
105
|
+
IUnitOfWork unitOfWork
|
|
106
|
+
) : IRequestHandler<CreateUserCommand, Result<Guid>>
|
|
107
|
+
{
|
|
108
|
+
public async Task<Result<Guid>> Handle(
|
|
109
|
+
CreateUserCommand request,
|
|
110
|
+
CancellationToken cancellationToken)
|
|
111
|
+
{
|
|
112
|
+
// Check for existing user
|
|
113
|
+
if (await userRepository.ExistsByEmailAsync(request.Email, cancellationToken))
|
|
114
|
+
{
|
|
115
|
+
return Result.Failure<Guid>(UserErrors.EmailAlreadyExists(request.Email));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Create user (factory method returns Result)
|
|
119
|
+
var userResult = User.Create(request.Email, request.Name, request.Password);
|
|
120
|
+
if (userResult.IsFailure)
|
|
121
|
+
{
|
|
122
|
+
return Result.Failure<Guid>(userResult.Error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await userRepository.AddAsync(userResult.Value, cancellationToken);
|
|
126
|
+
await unitOfWork.SaveChangesAsync(cancellationToken);
|
|
127
|
+
|
|
128
|
+
return userResult.Value.Id;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Domain Entity with Result
|
|
134
|
+
|
|
135
|
+
```csharp
|
|
136
|
+
// Domain/Users/User.cs
|
|
137
|
+
public class User : Entity
|
|
138
|
+
{
|
|
139
|
+
private User(Guid id, Email email, string name, PasswordHash passwordHash)
|
|
140
|
+
{
|
|
141
|
+
Id = id;
|
|
142
|
+
Email = email;
|
|
143
|
+
Name = name;
|
|
144
|
+
PasswordHash = passwordHash;
|
|
145
|
+
CreatedAt = DateTime.UtcNow;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public Email Email { get; private set; }
|
|
149
|
+
public string Name { get; private set; }
|
|
150
|
+
public PasswordHash PasswordHash { get; private set; }
|
|
151
|
+
public DateTime CreatedAt { get; }
|
|
152
|
+
public bool IsActive { get; private set; } = true;
|
|
153
|
+
|
|
154
|
+
public static Result<User> Create(string email, string name, string password)
|
|
155
|
+
{
|
|
156
|
+
// Validate email
|
|
157
|
+
var emailResult = Email.Create(email);
|
|
158
|
+
if (emailResult.IsFailure)
|
|
159
|
+
{
|
|
160
|
+
return Result.Failure<User>(emailResult.Error);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate name
|
|
164
|
+
if (string.IsNullOrWhiteSpace(name) || name.Length > 100)
|
|
165
|
+
{
|
|
166
|
+
return Result.Failure<User>(Error.Validation("Name", "Name is required and max 100 chars"));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Validate and hash password
|
|
170
|
+
var passwordResult = PasswordHash.Create(password);
|
|
171
|
+
if (passwordResult.IsFailure)
|
|
172
|
+
{
|
|
173
|
+
return Result.Failure<User>(passwordResult.Error);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return new User(Guid.NewGuid(), emailResult.Value, name, passwordResult.Value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public Result UpdateName(string newName)
|
|
180
|
+
{
|
|
181
|
+
if (string.IsNullOrWhiteSpace(newName) || newName.Length > 100)
|
|
182
|
+
{
|
|
183
|
+
return Result.Failure(Error.Validation("Name", "Name is required and max 100 chars"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
Name = newName;
|
|
187
|
+
return Result.Success();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public Result Deactivate()
|
|
191
|
+
{
|
|
192
|
+
if (!IsActive)
|
|
193
|
+
{
|
|
194
|
+
return Result.Failure(new Error("User.AlreadyDeactivated", "User is already deactivated"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
IsActive = false;
|
|
198
|
+
return Result.Success();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Value Objects with Result
|
|
204
|
+
|
|
205
|
+
```csharp
|
|
206
|
+
// Domain/Users/ValueObjects/Email.cs
|
|
207
|
+
public sealed class Email : ValueObject
|
|
208
|
+
{
|
|
209
|
+
private Email(string value) => Value = value;
|
|
210
|
+
|
|
211
|
+
public string Value { get; }
|
|
212
|
+
|
|
213
|
+
public static Result<Email> Create(string email)
|
|
214
|
+
{
|
|
215
|
+
if (string.IsNullOrWhiteSpace(email))
|
|
216
|
+
{
|
|
217
|
+
return Result.Failure<Email>(Error.Validation("Email", "Email is required"));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
email = email.Trim().ToLowerInvariant();
|
|
221
|
+
|
|
222
|
+
if (email.Length > 256)
|
|
223
|
+
{
|
|
224
|
+
return Result.Failure<Email>(Error.Validation("Email", "Email is too long"));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!IsValidEmail(email))
|
|
228
|
+
{
|
|
229
|
+
return Result.Failure<Email>(Error.Validation("Email", "Email format is invalid"));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return new Email(email);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private static bool IsValidEmail(string email) =>
|
|
236
|
+
Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
|
|
237
|
+
|
|
238
|
+
protected override IEnumerable<object> GetAtomicValues()
|
|
239
|
+
{
|
|
240
|
+
yield return Value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## API Response Mapping
|
|
246
|
+
|
|
247
|
+
```csharp
|
|
248
|
+
// WebApi/Extensions/ResultExtensions.cs
|
|
249
|
+
public static class ResultExtensions
|
|
250
|
+
{
|
|
251
|
+
public static IResult ToApiResult(this Result result) =>
|
|
252
|
+
result.IsSuccess
|
|
253
|
+
? Results.Ok()
|
|
254
|
+
: result.ToProblemDetails();
|
|
255
|
+
|
|
256
|
+
public static IResult ToApiResult<T>(this Result<T> result) =>
|
|
257
|
+
result.IsSuccess
|
|
258
|
+
? Results.Ok(result.Value)
|
|
259
|
+
: result.ToProblemDetails();
|
|
260
|
+
|
|
261
|
+
public static IResult ToCreatedResult<T>(
|
|
262
|
+
this Result<T> result,
|
|
263
|
+
string routeName,
|
|
264
|
+
Func<T, object> routeValues)
|
|
265
|
+
{
|
|
266
|
+
return result.IsSuccess
|
|
267
|
+
? Results.CreatedAtRoute(routeName, routeValues(result.Value), result.Value)
|
|
268
|
+
: result.ToProblemDetails();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private static IResult ToProblemDetails(this Result result)
|
|
272
|
+
{
|
|
273
|
+
var statusCode = result.Error.Code switch
|
|
274
|
+
{
|
|
275
|
+
var code when code.Contains("NotFound") => StatusCodes.Status404NotFound,
|
|
276
|
+
var code when code.Contains("Validation") => StatusCodes.Status400BadRequest,
|
|
277
|
+
var code when code.Contains("Conflict") => StatusCodes.Status409Conflict,
|
|
278
|
+
var code when code.Contains("Unauthorized") => StatusCodes.Status401Unauthorized,
|
|
279
|
+
var code when code.Contains("Forbidden") => StatusCodes.Status403Forbidden,
|
|
280
|
+
_ => StatusCodes.Status400BadRequest
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return Results.Problem(
|
|
284
|
+
title: result.Error.Code,
|
|
285
|
+
detail: result.Error.Description,
|
|
286
|
+
statusCode: statusCode);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Usage in Minimal API
|
|
291
|
+
app.MapPost("/api/users", async (CreateUserCommand command, ISender sender) =>
|
|
292
|
+
{
|
|
293
|
+
var result = await sender.Send(command);
|
|
294
|
+
return result.ToCreatedResult("GetUser", id => new { id });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
app.MapGet("/api/users/{id:guid}", async (Guid id, ISender sender) =>
|
|
298
|
+
{
|
|
299
|
+
var result = await sender.Send(new GetUserQuery(id));
|
|
300
|
+
return result.ToApiResult();
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Match Pattern (Functional)
|
|
305
|
+
|
|
306
|
+
```csharp
|
|
307
|
+
// Extensions for functional-style handling
|
|
308
|
+
public static class ResultMatchExtensions
|
|
309
|
+
{
|
|
310
|
+
public static T Match<T>(
|
|
311
|
+
this Result result,
|
|
312
|
+
Func<T> onSuccess,
|
|
313
|
+
Func<Error, T> onFailure) =>
|
|
314
|
+
result.IsSuccess ? onSuccess() : onFailure(result.Error);
|
|
315
|
+
|
|
316
|
+
public static TOut Match<TIn, TOut>(
|
|
317
|
+
this Result<TIn> result,
|
|
318
|
+
Func<TIn, TOut> onSuccess,
|
|
319
|
+
Func<Error, TOut> onFailure) =>
|
|
320
|
+
result.IsSuccess ? onSuccess(result.Value) : onFailure(result.Error);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Usage
|
|
324
|
+
var response = result.Match(
|
|
325
|
+
onSuccess: user => Results.Ok(user),
|
|
326
|
+
onFailure: error => Results.Problem(error.Description)
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Anti-Patterns
|
|
331
|
+
|
|
332
|
+
```csharp
|
|
333
|
+
// BAD: Throwing exceptions for business logic
|
|
334
|
+
public User GetUser(Guid id)
|
|
335
|
+
{
|
|
336
|
+
var user = _repository.GetById(id);
|
|
337
|
+
if (user is null)
|
|
338
|
+
throw new NotFoundException($"User {id} not found"); // Exception for flow control!
|
|
339
|
+
return user;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// GOOD: Return Result
|
|
343
|
+
public Result<User> GetUser(Guid id)
|
|
344
|
+
{
|
|
345
|
+
var user = _repository.GetById(id);
|
|
346
|
+
return user is null
|
|
347
|
+
? Result.Failure<User>(UserErrors.NotFound(id))
|
|
348
|
+
: Result.Success(user);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
// BAD: Ignoring Result
|
|
353
|
+
var result = await CreateUserAsync(command);
|
|
354
|
+
return result.Value; // Throws if failure!
|
|
355
|
+
|
|
356
|
+
// GOOD: Handle Result properly
|
|
357
|
+
var result = await CreateUserAsync(command);
|
|
358
|
+
if (result.IsFailure)
|
|
359
|
+
{
|
|
360
|
+
return Results.Problem(result.Error.Description);
|
|
361
|
+
}
|
|
362
|
+
return Results.Ok(result.Value);
|
|
363
|
+
```
|