agentic-team-templates 0.13.2 → 0.14.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 +6 -1
- package/package.json +1 -1
- package/src/index.js +22 -2
- package/src/index.test.js +5 -0
- package/templates/cpp-expert/.cursorrules/concurrency.md +211 -0
- package/templates/cpp-expert/.cursorrules/error-handling.md +170 -0
- package/templates/cpp-expert/.cursorrules/memory-and-ownership.md +220 -0
- package/templates/cpp-expert/.cursorrules/modern-cpp.md +211 -0
- package/templates/cpp-expert/.cursorrules/overview.md +87 -0
- package/templates/cpp-expert/.cursorrules/performance.md +223 -0
- package/templates/cpp-expert/.cursorrules/testing.md +230 -0
- package/templates/cpp-expert/.cursorrules/tooling.md +312 -0
- package/templates/cpp-expert/CLAUDE.md +242 -0
- package/templates/csharp-expert/.cursorrules/aspnet-core.md +311 -0
- package/templates/csharp-expert/.cursorrules/async-patterns.md +206 -0
- package/templates/csharp-expert/.cursorrules/dependency-injection.md +206 -0
- package/templates/csharp-expert/.cursorrules/error-handling.md +235 -0
- package/templates/csharp-expert/.cursorrules/language-features.md +204 -0
- package/templates/csharp-expert/.cursorrules/overview.md +92 -0
- package/templates/csharp-expert/.cursorrules/performance.md +251 -0
- package/templates/csharp-expert/.cursorrules/testing.md +282 -0
- package/templates/csharp-expert/.cursorrules/tooling.md +254 -0
- package/templates/csharp-expert/CLAUDE.md +360 -0
- package/templates/java-expert/.cursorrules/concurrency.md +209 -0
- package/templates/java-expert/.cursorrules/error-handling.md +205 -0
- package/templates/java-expert/.cursorrules/modern-java.md +216 -0
- package/templates/java-expert/.cursorrules/overview.md +81 -0
- package/templates/java-expert/.cursorrules/performance.md +239 -0
- package/templates/java-expert/.cursorrules/persistence.md +262 -0
- package/templates/java-expert/.cursorrules/spring-boot.md +262 -0
- package/templates/java-expert/.cursorrules/testing.md +272 -0
- package/templates/java-expert/.cursorrules/tooling.md +301 -0
- package/templates/java-expert/CLAUDE.md +325 -0
- package/templates/javascript-expert/.cursorrules/overview.md +5 -3
- package/templates/javascript-expert/.cursorrules/typescript-deep-dive.md +348 -0
- package/templates/javascript-expert/CLAUDE.md +34 -3
- package/templates/kotlin-expert/.cursorrules/coroutines.md +237 -0
- package/templates/kotlin-expert/.cursorrules/error-handling.md +149 -0
- package/templates/kotlin-expert/.cursorrules/frameworks.md +227 -0
- package/templates/kotlin-expert/.cursorrules/language-features.md +231 -0
- package/templates/kotlin-expert/.cursorrules/overview.md +77 -0
- package/templates/kotlin-expert/.cursorrules/performance.md +185 -0
- package/templates/kotlin-expert/.cursorrules/testing.md +213 -0
- package/templates/kotlin-expert/.cursorrules/tooling.md +258 -0
- package/templates/kotlin-expert/CLAUDE.md +276 -0
- package/templates/swift-expert/.cursorrules/concurrency.md +230 -0
- package/templates/swift-expert/.cursorrules/error-handling.md +213 -0
- package/templates/swift-expert/.cursorrules/language-features.md +246 -0
- package/templates/swift-expert/.cursorrules/overview.md +88 -0
- package/templates/swift-expert/.cursorrules/performance.md +260 -0
- package/templates/swift-expert/.cursorrules/swiftui.md +260 -0
- package/templates/swift-expert/.cursorrules/testing.md +286 -0
- package/templates/swift-expert/.cursorrules/tooling.md +285 -0
- package/templates/swift-expert/CLAUDE.md +275 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# C# Dependency Injection
|
|
2
|
+
|
|
3
|
+
The built-in Microsoft.Extensions.DependencyInjection container is the standard. Use it correctly.
|
|
4
|
+
|
|
5
|
+
## Service Lifetimes
|
|
6
|
+
|
|
7
|
+
```csharp
|
|
8
|
+
// Transient: new instance every time. Use for lightweight, stateless services.
|
|
9
|
+
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
|
|
10
|
+
|
|
11
|
+
// Scoped: one instance per request/scope. Use for DbContext, Unit of Work.
|
|
12
|
+
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
|
|
13
|
+
builder.Services.AddDbContext<AppDbContext>();
|
|
14
|
+
|
|
15
|
+
// Singleton: one instance for app lifetime. Use for caches, config, HTTP clients.
|
|
16
|
+
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
|
17
|
+
ConnectionMultiplexer.Connect(configuration.GetConnectionString("Redis")!));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### The Captive Dependency Problem
|
|
21
|
+
|
|
22
|
+
A singleton must NEVER depend on a scoped or transient service — it captures a stale instance.
|
|
23
|
+
|
|
24
|
+
```csharp
|
|
25
|
+
// WRONG: singleton captures scoped DbContext
|
|
26
|
+
public class CachedUserService // Registered as Singleton
|
|
27
|
+
{
|
|
28
|
+
private readonly AppDbContext _db; // Scoped! This instance lives forever now.
|
|
29
|
+
public CachedUserService(AppDbContext db) => _db = db;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// RIGHT: use IServiceScopeFactory to create scopes
|
|
33
|
+
public class CachedUserService
|
|
34
|
+
{
|
|
35
|
+
private readonly IServiceScopeFactory _scopeFactory;
|
|
36
|
+
|
|
37
|
+
public CachedUserService(IServiceScopeFactory scopeFactory)
|
|
38
|
+
=> _scopeFactory = scopeFactory;
|
|
39
|
+
|
|
40
|
+
public async Task<User?> GetUserAsync(int id)
|
|
41
|
+
{
|
|
42
|
+
using var scope = _scopeFactory.CreateScope();
|
|
43
|
+
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
44
|
+
return await db.Users.FindAsync(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Registration Patterns
|
|
50
|
+
|
|
51
|
+
```csharp
|
|
52
|
+
// Extension methods for clean composition root
|
|
53
|
+
public static class OrderServiceExtensions
|
|
54
|
+
{
|
|
55
|
+
public static IServiceCollection AddOrderServices(this IServiceCollection services)
|
|
56
|
+
{
|
|
57
|
+
services.AddScoped<IOrderRepository, OrderRepository>();
|
|
58
|
+
services.AddScoped<IOrderService, OrderService>();
|
|
59
|
+
services.AddScoped<IInventoryChecker, InventoryChecker>();
|
|
60
|
+
return services;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Program.cs stays clean
|
|
65
|
+
builder.Services.AddOrderServices();
|
|
66
|
+
builder.Services.AddNotificationServices();
|
|
67
|
+
builder.Services.AddAuthenticationServices(builder.Configuration);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Options Pattern
|
|
71
|
+
|
|
72
|
+
```csharp
|
|
73
|
+
// Strongly typed configuration
|
|
74
|
+
public class SmtpOptions
|
|
75
|
+
{
|
|
76
|
+
public const string SectionName = "Smtp";
|
|
77
|
+
|
|
78
|
+
public required string Host { get; init; }
|
|
79
|
+
public int Port { get; init; } = 587;
|
|
80
|
+
public required string Username { get; init; }
|
|
81
|
+
public required string Password { get; init; }
|
|
82
|
+
public bool UseSsl { get; init; } = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Registration with validation
|
|
86
|
+
builder.Services
|
|
87
|
+
.AddOptions<SmtpOptions>()
|
|
88
|
+
.BindConfiguration(SmtpOptions.SectionName)
|
|
89
|
+
.ValidateDataAnnotations()
|
|
90
|
+
.ValidateOnStart();
|
|
91
|
+
|
|
92
|
+
// Injection — use IOptions<T> for static config, IOptionsMonitor<T> for reloadable
|
|
93
|
+
public class EmailSender(IOptions<SmtpOptions> options)
|
|
94
|
+
{
|
|
95
|
+
private readonly SmtpOptions _smtp = options.Value;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Keyed Services (.NET 8+)
|
|
100
|
+
|
|
101
|
+
```csharp
|
|
102
|
+
// Register multiple implementations of the same interface
|
|
103
|
+
builder.Services.AddKeyedSingleton<INotifier, EmailNotifier>("email");
|
|
104
|
+
builder.Services.AddKeyedSingleton<INotifier, SmsNotifier>("sms");
|
|
105
|
+
builder.Services.AddKeyedSingleton<INotifier, SlackNotifier>("slack");
|
|
106
|
+
|
|
107
|
+
// Inject by key
|
|
108
|
+
public class NotificationService([FromKeyedServices("email")] INotifier emailNotifier)
|
|
109
|
+
{
|
|
110
|
+
public Task NotifyAsync(string message)
|
|
111
|
+
=> emailNotifier.SendAsync(message);
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Interface Segregation
|
|
116
|
+
|
|
117
|
+
```csharp
|
|
118
|
+
// Bad: fat interface
|
|
119
|
+
public interface IUserRepository
|
|
120
|
+
{
|
|
121
|
+
Task<User?> GetByIdAsync(int id, CancellationToken ct);
|
|
122
|
+
Task<User?> GetByEmailAsync(string email, CancellationToken ct);
|
|
123
|
+
Task<IReadOnlyList<User>> GetAllAsync(CancellationToken ct);
|
|
124
|
+
Task CreateAsync(User user, CancellationToken ct);
|
|
125
|
+
Task UpdateAsync(User user, CancellationToken ct);
|
|
126
|
+
Task DeleteAsync(int id, CancellationToken ct);
|
|
127
|
+
Task<int> GetCountAsync(CancellationToken ct);
|
|
128
|
+
Task<bool> ExistsAsync(string email, CancellationToken ct);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Good: segregated interfaces
|
|
132
|
+
public interface IUserReader
|
|
133
|
+
{
|
|
134
|
+
Task<User?> GetByIdAsync(int id, CancellationToken ct);
|
|
135
|
+
Task<User?> GetByEmailAsync(string email, CancellationToken ct);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public interface IUserWriter
|
|
139
|
+
{
|
|
140
|
+
Task CreateAsync(User user, CancellationToken ct);
|
|
141
|
+
Task UpdateAsync(User user, CancellationToken ct);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Consumers only depend on what they use
|
|
145
|
+
public class LoginService(IUserReader users) { }
|
|
146
|
+
public class RegistrationService(IUserReader users, IUserWriter writer) { }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Decorator Pattern
|
|
150
|
+
|
|
151
|
+
```csharp
|
|
152
|
+
// Scrutor or manual registration for decorators
|
|
153
|
+
public class CachedUserRepository : IUserReader
|
|
154
|
+
{
|
|
155
|
+
private readonly IUserReader _inner;
|
|
156
|
+
private readonly IMemoryCache _cache;
|
|
157
|
+
|
|
158
|
+
public CachedUserRepository(IUserReader inner, IMemoryCache cache)
|
|
159
|
+
{
|
|
160
|
+
_inner = inner;
|
|
161
|
+
_cache = cache;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async Task<User?> GetByIdAsync(int id, CancellationToken ct)
|
|
165
|
+
{
|
|
166
|
+
return await _cache.GetOrCreateAsync($"user:{id}", async entry =>
|
|
167
|
+
{
|
|
168
|
+
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
|
|
169
|
+
return await _inner.GetByIdAsync(id, ct);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Registration
|
|
175
|
+
builder.Services.AddScoped<UserRepository>();
|
|
176
|
+
builder.Services.AddScoped<IUserReader>(sp =>
|
|
177
|
+
new CachedUserRepository(
|
|
178
|
+
sp.GetRequiredService<UserRepository>(),
|
|
179
|
+
sp.GetRequiredService<IMemoryCache>()));
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Anti-Patterns
|
|
183
|
+
|
|
184
|
+
```csharp
|
|
185
|
+
// Never: service locator pattern
|
|
186
|
+
public class OrderService
|
|
187
|
+
{
|
|
188
|
+
private readonly IServiceProvider _sp;
|
|
189
|
+
public OrderService(IServiceProvider sp) => _sp = sp;
|
|
190
|
+
public void Process()
|
|
191
|
+
{
|
|
192
|
+
var repo = _sp.GetRequiredService<IOrderRepository>(); // Hidden dependency
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Use constructor injection instead
|
|
196
|
+
|
|
197
|
+
// Never: registering concrete types without interfaces (for testability)
|
|
198
|
+
builder.Services.AddScoped<OrderService>(); // Can't mock in tests
|
|
199
|
+
// Register with interface: AddScoped<IOrderService, OrderService>();
|
|
200
|
+
|
|
201
|
+
// Never: static service accessors
|
|
202
|
+
public static class ServiceLocator
|
|
203
|
+
{
|
|
204
|
+
public static IServiceProvider Provider { get; set; } = null!; // Global mutable state
|
|
205
|
+
}
|
|
206
|
+
```
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# C# Error Handling
|
|
2
|
+
|
|
3
|
+
Exceptions for exceptional conditions. Result types for expected failures. Never swallow errors.
|
|
4
|
+
|
|
5
|
+
## The Two Categories
|
|
6
|
+
|
|
7
|
+
1. **Exceptions** — Programming errors, infrastructure failures, genuinely unexpected conditions
|
|
8
|
+
2. **Result patterns** — Validation failures, business rule violations, "not found" scenarios
|
|
9
|
+
|
|
10
|
+
## Exception Best Practices
|
|
11
|
+
|
|
12
|
+
```csharp
|
|
13
|
+
// Catch specific exceptions, never bare catch
|
|
14
|
+
try
|
|
15
|
+
{
|
|
16
|
+
await _httpClient.PostAsync(url, content, ct);
|
|
17
|
+
}
|
|
18
|
+
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
|
|
19
|
+
{
|
|
20
|
+
_logger.LogWarning(ex, "Rate limited by {Url}", url);
|
|
21
|
+
await Task.Delay(retryDelay, ct);
|
|
22
|
+
}
|
|
23
|
+
catch (TaskCanceledException) when (ct.IsCancellationRequested)
|
|
24
|
+
{
|
|
25
|
+
_logger.LogInformation("Request cancelled");
|
|
26
|
+
throw; // Let cancellation propagate
|
|
27
|
+
}
|
|
28
|
+
catch (HttpRequestException ex)
|
|
29
|
+
{
|
|
30
|
+
_logger.LogError(ex, "HTTP request to {Url} failed", url);
|
|
31
|
+
throw; // Re-throw — don't swallow
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Exception filters (when clause) — use them for conditional catch
|
|
35
|
+
catch (SqlException ex) when (ex.Number == 2627) // Unique constraint violation
|
|
36
|
+
{
|
|
37
|
+
throw new DuplicateEntityException("User with this email already exists", ex);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Guard Clauses
|
|
42
|
+
|
|
43
|
+
```csharp
|
|
44
|
+
public class OrderService
|
|
45
|
+
{
|
|
46
|
+
public async Task<Order> CreateAsync(CreateOrderRequest request, CancellationToken ct)
|
|
47
|
+
{
|
|
48
|
+
ArgumentNullException.ThrowIfNull(request);
|
|
49
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(request.CustomerId);
|
|
50
|
+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(request.Items.Count);
|
|
51
|
+
|
|
52
|
+
// Business logic follows clean preconditions
|
|
53
|
+
var customer = await _customers.GetByIdAsync(request.CustomerId, ct)
|
|
54
|
+
?? throw new NotFoundException($"Customer '{request.CustomerId}' not found");
|
|
55
|
+
|
|
56
|
+
return Order.Create(customer, request.Items);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Result Pattern
|
|
62
|
+
|
|
63
|
+
```csharp
|
|
64
|
+
// A generic Result type for operations that can fail predictably
|
|
65
|
+
public readonly record struct Result<T>
|
|
66
|
+
{
|
|
67
|
+
public T? Value { get; }
|
|
68
|
+
public Error? Error { get; }
|
|
69
|
+
public bool IsSuccess => Error is null;
|
|
70
|
+
|
|
71
|
+
private Result(T value) { Value = value; Error = null; }
|
|
72
|
+
private Result(Error error) { Value = default; Error = error; }
|
|
73
|
+
|
|
74
|
+
public static Result<T> Success(T value) => new(value);
|
|
75
|
+
public static Result<T> Failure(Error error) => new(error);
|
|
76
|
+
|
|
77
|
+
public TResult Match<TResult>(
|
|
78
|
+
Func<T, TResult> onSuccess,
|
|
79
|
+
Func<Error, TResult> onFailure)
|
|
80
|
+
=> IsSuccess ? onSuccess(Value!) : onFailure(Error!);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public record Error(string Code, string Message)
|
|
84
|
+
{
|
|
85
|
+
public static Error NotFound(string entity, object id)
|
|
86
|
+
=> new("NOT_FOUND", $"{entity} with id '{id}' was not found");
|
|
87
|
+
|
|
88
|
+
public static Error Validation(string message)
|
|
89
|
+
=> new("VALIDATION", message);
|
|
90
|
+
|
|
91
|
+
public static Error Conflict(string message)
|
|
92
|
+
=> new("CONFLICT", message);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Using the Result Pattern
|
|
97
|
+
|
|
98
|
+
```csharp
|
|
99
|
+
public class UserService
|
|
100
|
+
{
|
|
101
|
+
public async Task<Result<User>> RegisterAsync(
|
|
102
|
+
RegisterRequest request, CancellationToken ct)
|
|
103
|
+
{
|
|
104
|
+
// Validation
|
|
105
|
+
if (!EmailValidator.IsValid(request.Email))
|
|
106
|
+
return Result<User>.Failure(Error.Validation("Invalid email format"));
|
|
107
|
+
|
|
108
|
+
// Business rule check
|
|
109
|
+
var existing = await _users.GetByEmailAsync(request.Email, ct);
|
|
110
|
+
if (existing is not null)
|
|
111
|
+
return Result<User>.Failure(Error.Conflict("Email already registered"));
|
|
112
|
+
|
|
113
|
+
// Happy path
|
|
114
|
+
var user = User.Create(request.Name, request.Email);
|
|
115
|
+
await _users.CreateAsync(user, ct);
|
|
116
|
+
return Result<User>.Success(user);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// In the endpoint
|
|
121
|
+
app.MapPost("/users", async (RegisterRequest request, UserService service, CancellationToken ct) =>
|
|
122
|
+
{
|
|
123
|
+
var result = await service.RegisterAsync(request, ct);
|
|
124
|
+
return result.Match(
|
|
125
|
+
user => Results.Created($"/users/{user.Id}", user),
|
|
126
|
+
error => error.Code switch
|
|
127
|
+
{
|
|
128
|
+
"VALIDATION" => Results.BadRequest(error),
|
|
129
|
+
"CONFLICT" => Results.Conflict(error),
|
|
130
|
+
_ => Results.Problem(error.Message)
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Problem Details (RFC 9457)
|
|
136
|
+
|
|
137
|
+
```csharp
|
|
138
|
+
// ASP.NET Core's built-in Problem Details support
|
|
139
|
+
builder.Services.AddProblemDetails(options =>
|
|
140
|
+
{
|
|
141
|
+
options.CustomizeProblemDetails = context =>
|
|
142
|
+
{
|
|
143
|
+
context.ProblemDetails.Extensions["traceId"] =
|
|
144
|
+
context.HttpContext.TraceIdentifier;
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Global exception handler middleware
|
|
149
|
+
app.UseExceptionHandler(exceptionApp =>
|
|
150
|
+
{
|
|
151
|
+
exceptionApp.Run(async context =>
|
|
152
|
+
{
|
|
153
|
+
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
|
|
154
|
+
var problemDetails = exception switch
|
|
155
|
+
{
|
|
156
|
+
NotFoundException ex => new ProblemDetails
|
|
157
|
+
{
|
|
158
|
+
Status = 404,
|
|
159
|
+
Title = "Not Found",
|
|
160
|
+
Detail = ex.Message
|
|
161
|
+
},
|
|
162
|
+
ValidationException ex => new ProblemDetails
|
|
163
|
+
{
|
|
164
|
+
Status = 400,
|
|
165
|
+
Title = "Validation Error",
|
|
166
|
+
Detail = ex.Message
|
|
167
|
+
},
|
|
168
|
+
_ => new ProblemDetails
|
|
169
|
+
{
|
|
170
|
+
Status = 500,
|
|
171
|
+
Title = "Internal Server Error",
|
|
172
|
+
Detail = "An unexpected error occurred"
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
context.Response.StatusCode = problemDetails.Status ?? 500;
|
|
177
|
+
await context.Response.WriteAsJsonAsync(problemDetails);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## FluentValidation
|
|
183
|
+
|
|
184
|
+
```csharp
|
|
185
|
+
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
|
|
186
|
+
{
|
|
187
|
+
public CreateOrderValidator()
|
|
188
|
+
{
|
|
189
|
+
RuleFor(x => x.CustomerId)
|
|
190
|
+
.NotEmpty()
|
|
191
|
+
.WithMessage("Customer ID is required");
|
|
192
|
+
|
|
193
|
+
RuleFor(x => x.Items)
|
|
194
|
+
.NotEmpty()
|
|
195
|
+
.WithMessage("Order must contain at least one item");
|
|
196
|
+
|
|
197
|
+
RuleForEach(x => x.Items).ChildRules(item =>
|
|
198
|
+
{
|
|
199
|
+
item.RuleFor(i => i.Quantity)
|
|
200
|
+
.GreaterThan(0)
|
|
201
|
+
.WithMessage("Quantity must be positive");
|
|
202
|
+
|
|
203
|
+
item.RuleFor(i => i.Price)
|
|
204
|
+
.GreaterThan(0)
|
|
205
|
+
.WithMessage("Price must be positive");
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Anti-Patterns
|
|
212
|
+
|
|
213
|
+
```csharp
|
|
214
|
+
// Never: catch-all that swallows exceptions
|
|
215
|
+
try { DoWork(); }
|
|
216
|
+
catch (Exception) { } // Silent failure — a bug hiding a bug
|
|
217
|
+
|
|
218
|
+
// Never: catch and throw new (loses stack trace)
|
|
219
|
+
catch (Exception ex) { throw new Exception("Failed", ex); }
|
|
220
|
+
// Use: throw; (preserves stack trace)
|
|
221
|
+
// Or: throw new SpecificException("context", ex); (wraps with context)
|
|
222
|
+
|
|
223
|
+
// Never: exceptions for control flow
|
|
224
|
+
try { return dict[key]; }
|
|
225
|
+
catch (KeyNotFoundException) { return default; }
|
|
226
|
+
// Use: dict.TryGetValue(key, out var value)
|
|
227
|
+
|
|
228
|
+
// Never: log and throw (produces duplicate log entries)
|
|
229
|
+
catch (Exception ex)
|
|
230
|
+
{
|
|
231
|
+
_logger.LogError(ex, "Failed");
|
|
232
|
+
throw; // Now it's logged twice — here and in global handler
|
|
233
|
+
}
|
|
234
|
+
// Choose one: log OR throw, not both
|
|
235
|
+
```
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Modern C# Language Features
|
|
2
|
+
|
|
3
|
+
Deep knowledge of C# language evolution. Use modern features deliberately, not just because they exist.
|
|
4
|
+
|
|
5
|
+
## Nullable Reference Types
|
|
6
|
+
|
|
7
|
+
The single most impactful feature for code correctness. Non-negotiable.
|
|
8
|
+
|
|
9
|
+
```csharp
|
|
10
|
+
// The compiler is your partner — listen to it
|
|
11
|
+
public class UserService
|
|
12
|
+
{
|
|
13
|
+
// Non-nullable: guaranteed to have a value
|
|
14
|
+
private readonly IUserRepository _repository;
|
|
15
|
+
|
|
16
|
+
// Nullable: explicitly communicates "might not exist"
|
|
17
|
+
public async Task<User?> FindByEmailAsync(string email)
|
|
18
|
+
{
|
|
19
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(email);
|
|
20
|
+
return await _repository.FindByEmailAsync(email);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Null-forgiving operator (!) — document WHY
|
|
24
|
+
// Only when you genuinely know better than the compiler
|
|
25
|
+
var user = users.FirstOrDefault(u => u.Id == id)!; // Validated above
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Rules
|
|
30
|
+
|
|
31
|
+
- Never disable nullable warnings project-wide
|
|
32
|
+
- `string` means non-null. `string?` means nullable. Respect the distinction
|
|
33
|
+
- Use `ArgumentNullException.ThrowIfNull()` at public API boundaries
|
|
34
|
+
- Avoid the null-forgiving operator (`!`) — if you need it, the design may be wrong
|
|
35
|
+
|
|
36
|
+
## Records
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
// Immutable data with value semantics — the default for DTOs and value objects
|
|
40
|
+
public record CreateUserRequest(string Name, string Email);
|
|
41
|
+
|
|
42
|
+
// Record structs for high-performance value types
|
|
43
|
+
public readonly record struct Coordinate(double Latitude, double Longitude);
|
|
44
|
+
|
|
45
|
+
// Records with validation
|
|
46
|
+
public record Money
|
|
47
|
+
{
|
|
48
|
+
public decimal Amount { get; }
|
|
49
|
+
public string Currency { get; }
|
|
50
|
+
|
|
51
|
+
public Money(decimal amount, string currency)
|
|
52
|
+
{
|
|
53
|
+
ArgumentOutOfRangeException.ThrowIfNegative(amount);
|
|
54
|
+
ArgumentException.ThrowIfNullOrWhiteSpace(currency);
|
|
55
|
+
Amount = amount;
|
|
56
|
+
Currency = currency.ToUpperInvariant();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Non-destructive mutation
|
|
61
|
+
var updated = original with { Email = "new@example.com" };
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Pattern Matching
|
|
65
|
+
|
|
66
|
+
```csharp
|
|
67
|
+
// Switch expressions — exhaustive, concise
|
|
68
|
+
public static string FormatStatus(OrderStatus status) => status switch
|
|
69
|
+
{
|
|
70
|
+
OrderStatus.Pending => "Awaiting processing",
|
|
71
|
+
OrderStatus.Shipped => "On its way",
|
|
72
|
+
OrderStatus.Delivered => "Delivered",
|
|
73
|
+
OrderStatus.Cancelled => "Cancelled",
|
|
74
|
+
_ => throw new ArgumentOutOfRangeException(nameof(status))
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Property patterns
|
|
78
|
+
public static decimal CalculateDiscount(Order order) => order switch
|
|
79
|
+
{
|
|
80
|
+
{ Total: > 1000, Customer.IsPremium: true } => order.Total * 0.15m,
|
|
81
|
+
{ Total: > 500 } => order.Total * 0.10m,
|
|
82
|
+
{ Customer.IsPremium: true } => order.Total * 0.05m,
|
|
83
|
+
_ => 0m
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Type patterns with guards
|
|
87
|
+
public static string Describe(object value) => value switch
|
|
88
|
+
{
|
|
89
|
+
int n when n < 0 => "negative integer",
|
|
90
|
+
int n => $"positive integer: {n}",
|
|
91
|
+
string { Length: 0 } => "empty string",
|
|
92
|
+
string s => $"string: {s}",
|
|
93
|
+
null => "null",
|
|
94
|
+
_ => $"unknown: {value.GetType().Name}"
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// List patterns (C# 11+)
|
|
98
|
+
public static bool IsValidSequence(int[] values) => values switch
|
|
99
|
+
{
|
|
100
|
+
[1, 2, 3] => true,
|
|
101
|
+
[1, .., 3] => true, // Starts with 1, ends with 3
|
|
102
|
+
[_, _, ..] => true, // At least 2 elements
|
|
103
|
+
_ => false
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## LINQ — Use It Well
|
|
108
|
+
|
|
109
|
+
```csharp
|
|
110
|
+
// Good: clear, composable, declarative
|
|
111
|
+
var activeUsers = users
|
|
112
|
+
.Where(u => u.IsActive)
|
|
113
|
+
.OrderBy(u => u.LastLogin)
|
|
114
|
+
.Select(u => new UserSummary(u.Id, u.Name, u.Email))
|
|
115
|
+
.ToList();
|
|
116
|
+
|
|
117
|
+
// Bad: LINQ for side effects
|
|
118
|
+
users.ForEach(u => u.Deactivate()); // Use a foreach loop instead
|
|
119
|
+
|
|
120
|
+
// Bad: multiple enumerations of IEnumerable
|
|
121
|
+
var count = users.Count(); // Enumerates
|
|
122
|
+
var first = users.FirstOrDefault(); // Enumerates again!
|
|
123
|
+
// Fix: materialize first with .ToList()
|
|
124
|
+
|
|
125
|
+
// Performance: use the right method
|
|
126
|
+
users.Any(u => u.IsAdmin) // Good: short-circuits
|
|
127
|
+
users.Count(u => u.IsAdmin) > 0 // Bad: counts everything
|
|
128
|
+
users.Where(u => u.IsAdmin).Any() // Acceptable but unnecessary
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Spans and Memory
|
|
132
|
+
|
|
133
|
+
```csharp
|
|
134
|
+
// Span<T> for zero-allocation slicing
|
|
135
|
+
public static bool StartsWithDigit(ReadOnlySpan<char> input)
|
|
136
|
+
=> !input.IsEmpty && char.IsDigit(input[0]);
|
|
137
|
+
|
|
138
|
+
// String parsing without allocation
|
|
139
|
+
public static (string Key, string Value) ParseHeader(ReadOnlySpan<char> header)
|
|
140
|
+
{
|
|
141
|
+
var separatorIndex = header.IndexOf(':');
|
|
142
|
+
if (separatorIndex < 0)
|
|
143
|
+
throw new FormatException("Invalid header format");
|
|
144
|
+
|
|
145
|
+
var key = header[..separatorIndex].Trim().ToString();
|
|
146
|
+
var value = header[(separatorIndex + 1)..].Trim().ToString();
|
|
147
|
+
return (key, value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// stackalloc for small buffers
|
|
151
|
+
Span<byte> buffer = stackalloc byte[256];
|
|
152
|
+
var bytesWritten = Encoding.UTF8.GetBytes(input, buffer);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Collection Expressions (C# 12+)
|
|
156
|
+
|
|
157
|
+
```csharp
|
|
158
|
+
// Concise collection initialization
|
|
159
|
+
int[] numbers = [1, 2, 3, 4, 5];
|
|
160
|
+
List<string> names = ["Alice", "Bob"];
|
|
161
|
+
Dictionary<string, int> scores = new() { ["Alice"] = 95, ["Bob"] = 87 };
|
|
162
|
+
|
|
163
|
+
// Spread operator
|
|
164
|
+
int[] combined = [..firstHalf, ..secondHalf];
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Primary Constructors (C# 12+)
|
|
168
|
+
|
|
169
|
+
```csharp
|
|
170
|
+
// For services — captures parameters as fields
|
|
171
|
+
public class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
|
|
172
|
+
{
|
|
173
|
+
public async Task<Order> GetByIdAsync(int id)
|
|
174
|
+
{
|
|
175
|
+
logger.LogInformation("Fetching order {OrderId}", id);
|
|
176
|
+
return await repository.GetByIdAsync(id)
|
|
177
|
+
?? throw new NotFoundException($"Order {id} not found");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Caution: primary constructor parameters are mutable and capturable
|
|
182
|
+
// For DTOs, prefer records instead
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Anti-Patterns
|
|
186
|
+
|
|
187
|
+
```csharp
|
|
188
|
+
// Never: stringly-typed code
|
|
189
|
+
void Process(string type, string data) { } // What types? What format?
|
|
190
|
+
// Use enums, records, or discriminated unions
|
|
191
|
+
|
|
192
|
+
// Never: throwing exceptions for control flow
|
|
193
|
+
try { return users.First(u => u.Id == id); }
|
|
194
|
+
catch (InvalidOperationException) { return null; }
|
|
195
|
+
// Use FirstOrDefault or TryGetValue patterns
|
|
196
|
+
|
|
197
|
+
// Never: mutable static state
|
|
198
|
+
static List<User> _cache = new(); // Thread-unsafe, untestable
|
|
199
|
+
// Use IMemoryCache or a proper caching abstraction
|
|
200
|
+
|
|
201
|
+
// Never: deep inheritance hierarchies
|
|
202
|
+
class SpecialPremiumInternationalCustomerOrder : PremiumCustomerOrder { }
|
|
203
|
+
// Use composition and interfaces
|
|
204
|
+
```
|