Skip to content

Commit ea41348

Browse files
committed
refactor: apply dependency injection pattern across command and query handlers
1 parent 28153c2 commit ea41348

15 files changed

Lines changed: 145 additions & 63 deletions

File tree

CLAUDE.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,45 @@ Remember: PRDs help ensure features are built to meet user needs and business ob
7575
- Remove any un-needed whitespace
7676
- Simplify collection initializations where possible
7777

78+
## Dependency Injection Pattern
79+
80+
**IMPORTANT**: All services, handlers, and classes using dependency injection MUST follow this pattern:
81+
82+
1. **Use primary constructors** for dependency injection
83+
2. **Create private readonly fields** for ALL injected dependencies
84+
3. **Prefix fields with underscore** (e.g., `_userRepository`, `_logger`)
85+
4. **Initialize fields from constructor parameters**
86+
87+
Example:
88+
```csharp
89+
public class SomeService(
90+
IUserRepository userRepository,
91+
IUnitOfWork unitOfWork,
92+
IMapper mapper,
93+
ILogger<SomeService> logger) : ISomeService
94+
{
95+
private readonly IUserRepository _userRepository = userRepository;
96+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
97+
private readonly IMapper _mapper = mapper;
98+
private readonly ILogger<SomeService> _logger = logger;
99+
100+
public async Task DoSomething()
101+
{
102+
// Use _userRepository, _unitOfWork, etc. (NOT userRepository)
103+
User? user = await _userRepository.GetByIdAsync(id);
104+
_logger.LogInformation("Did something");
105+
}
106+
}
107+
```
108+
109+
This pattern applies to:
110+
- All service implementations (IUserService, ITeslaApiService, etc.)
111+
- All MediatR command and query handlers
112+
- All authentication handlers
113+
- Any class using constructor dependency injection
114+
115+
**Never** use constructor parameters directly in methods - always use the private readonly fields.
116+
78117
4. **Modern C# patterns**:
79118
- **Use expression-bodied members** for methods/properties that can be expressed as a single expression
80119
- **Formatting**: Place `=>` on the same line as the method signature

src/services/TeslaStarter.Application/Users/Commands/CreateUser/CreateUserCommandHandler.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ public sealed class CreateUserCommandHandler(
88
IMapper mapper,
99
ILogger<CreateUserCommandHandler> logger) : IRequestHandler<CreateUserCommand, UserDto>
1010
{
11+
private readonly IUserRepository _userRepository = userRepository;
12+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
13+
private readonly IMapper _mapper = mapper;
14+
private readonly ILogger<CreateUserCommandHandler> _logger = logger;
15+
1116
public async Task<UserDto> Handle(CreateUserCommand request, CancellationToken cancellationToken)
1217
{
1318
// Check if user already exists with external ID
14-
User? existingUserByExternalId = await userRepository.GetByExternalIdAsync(
19+
User? existingUserByExternalId = await _userRepository.GetByExternalIdAsync(
1520
ExternalId.Create(request.ExternalId),
1621
cancellationToken);
1722

@@ -25,7 +30,7 @@ public async Task<UserDto> Handle(CreateUserCommand request, CancellationToken c
2530
}
2631

2732
// Check if user already exists with email
28-
User? existingUserByEmail = await userRepository.GetByEmailAsync(
33+
User? existingUserByEmail = await _userRepository.GetByEmailAsync(
2934
Email.Create(request.Email),
3035
cancellationToken);
3136

@@ -44,13 +49,13 @@ public async Task<UserDto> Handle(CreateUserCommand request, CancellationToken c
4449
request.Email,
4550
request.DisplayName);
4651

47-
userRepository.Add(user);
52+
_userRepository.Add(user);
4853

49-
await unitOfWork.SaveChangesAsync(cancellationToken);
54+
await _unitOfWork.SaveChangesAsync(cancellationToken);
5055

51-
logger.LogInformation("Created user {UserId} with external ID {ExternalId}",
56+
_logger.LogInformation("Created user {UserId} with external ID {ExternalId}",
5257
user.Id.Value, user.ExternalId.Value);
5358

54-
return mapper.Map<UserDto>(user);
59+
return _mapper.Map<UserDto>(user);
5560
}
5661
}

src/services/TeslaStarter.Application/Users/Commands/LinkTeslaAccount/LinkTeslaAccountCommandHandler.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ public sealed class LinkTeslaAccountCommandHandler(
99
IMapper mapper,
1010
ILogger<LinkTeslaAccountCommandHandler> logger) : IRequestHandler<LinkTeslaAccountCommand, UserDto>
1111
{
12+
private readonly IUserRepository _userRepository = userRepository;
13+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
14+
private readonly IMapper _mapper = mapper;
15+
private readonly ILogger<LinkTeslaAccountCommandHandler> _logger = logger;
1216
public async Task<UserDto> Handle(LinkTeslaAccountCommand request, CancellationToken cancellationToken)
1317
{
14-
User? user = await userRepository.GetByIdAsync(
18+
User? user = await _userRepository.GetByIdAsync(
1519
new UserId(request.UserId),
1620
cancellationToken) ?? throw new NotFoundException(nameof(User), request.UserId);
1721

@@ -28,12 +32,12 @@ public async Task<UserDto> Handle(LinkTeslaAccountCommand request, CancellationT
2832
]);
2933
}
3034

31-
userRepository.Update(user);
32-
await unitOfWork.SaveChangesAsync(cancellationToken);
35+
_userRepository.Update(user);
36+
await _unitOfWork.SaveChangesAsync(cancellationToken);
3337

34-
logger.LogInformation("Linked Tesla account {TeslaAccountId} to user {UserId}",
38+
_logger.LogInformation("Linked Tesla account {TeslaAccountId} to user {UserId}",
3539
request.TeslaAccountId, user.Id.Value);
3640

37-
return mapper.Map<UserDto>(user);
41+
return _mapper.Map<UserDto>(user);
3842
}
3943
}

src/services/TeslaStarter.Application/Users/Commands/RecordLogin/RecordLoginCommandHandler.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,23 @@ public sealed class RecordLoginCommandHandler(
99
IMapper mapper,
1010
ILogger<RecordLoginCommandHandler> logger) : IRequestHandler<RecordLoginCommand, UserDto>
1111
{
12+
private readonly IUserRepository _userRepository = userRepository;
13+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
14+
private readonly IMapper _mapper = mapper;
15+
private readonly ILogger<RecordLoginCommandHandler> _logger = logger;
1216
public async Task<UserDto> Handle(RecordLoginCommand request, CancellationToken cancellationToken)
1317
{
14-
User? user = await userRepository.GetByIdAsync(
18+
User? user = await _userRepository.GetByIdAsync(
1519
new UserId(request.UserId),
1620
cancellationToken) ?? throw new NotFoundException(nameof(User), request.UserId);
1721

1822
user.RecordLogin();
1923

20-
userRepository.Update(user);
21-
await unitOfWork.SaveChangesAsync(cancellationToken);
24+
_userRepository.Update(user);
25+
await _unitOfWork.SaveChangesAsync(cancellationToken);
2226

23-
logger.LogInformation("Recorded login for user {UserId}", user.Id.Value);
27+
_logger.LogInformation("Recorded login for user {UserId}", user.Id.Value);
2428

25-
return mapper.Map<UserDto>(user);
29+
return _mapper.Map<UserDto>(user);
2630
}
2731
}

src/services/TeslaStarter.Application/Users/Commands/UnlinkTeslaAccount/UnlinkTeslaAccountCommandHandler.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ public sealed class UnlinkTeslaAccountCommandHandler(
99
IMapper mapper,
1010
ILogger<UnlinkTeslaAccountCommandHandler> logger) : IRequestHandler<UnlinkTeslaAccountCommand, UserDto>
1111
{
12+
private readonly IUserRepository _userRepository = userRepository;
13+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
14+
private readonly IMapper _mapper = mapper;
15+
private readonly ILogger<UnlinkTeslaAccountCommandHandler> _logger = logger;
1216
public async Task<UserDto> Handle(UnlinkTeslaAccountCommand request, CancellationToken cancellationToken)
1317
{
14-
User user = await userRepository.GetByIdAsync(
18+
User user = await _userRepository.GetByIdAsync(
1519
new UserId(request.UserId),
1620
cancellationToken) ?? throw new NotFoundException(nameof(User), request.UserId);
1721

@@ -28,11 +32,11 @@ public async Task<UserDto> Handle(UnlinkTeslaAccountCommand request, Cancellatio
2832
]);
2933
}
3034

31-
userRepository.Update(user);
32-
await unitOfWork.SaveChangesAsync(cancellationToken);
35+
_userRepository.Update(user);
36+
await _unitOfWork.SaveChangesAsync(cancellationToken);
3337

34-
logger.LogInformation("Unlinked Tesla account from user {UserId}", user.Id.Value);
38+
_logger.LogInformation("Unlinked Tesla account from user {UserId}", user.Id.Value);
3539

36-
return mapper.Map<UserDto>(user);
40+
return _mapper.Map<UserDto>(user);
3741
}
3842
}

src/services/TeslaStarter.Application/Users/Commands/UpdateProfile/UpdateProfileCommandHandler.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ public sealed class UpdateProfileCommandHandler(
99
IMapper mapper,
1010
ILogger<UpdateProfileCommandHandler> logger) : IRequestHandler<UpdateProfileCommand, UserDto>
1111
{
12+
private readonly IUserRepository _userRepository = userRepository;
13+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
14+
private readonly IMapper _mapper = mapper;
15+
private readonly ILogger<UpdateProfileCommandHandler> _logger = logger;
1216
public async Task<UserDto> Handle(UpdateProfileCommand request, CancellationToken cancellationToken)
1317
{
14-
User user = await userRepository.GetByIdAsync(
18+
User user = await _userRepository.GetByIdAsync(
1519
new UserId(request.UserId),
1620
cancellationToken) ?? throw new NotFoundException(nameof(User), request.UserId);
1721

1822
// Check if another user already has this email
19-
User? existingUserWithEmail = await userRepository.GetByEmailAsync(
23+
User? existingUserWithEmail = await _userRepository.GetByEmailAsync(
2024
Email.Create(request.Email),
2125
cancellationToken);
2226

@@ -31,11 +35,11 @@ public async Task<UserDto> Handle(UpdateProfileCommand request, CancellationToke
3135

3236
user.UpdateProfile(request.Email, request.DisplayName);
3337

34-
userRepository.Update(user);
35-
await unitOfWork.SaveChangesAsync(cancellationToken);
38+
_userRepository.Update(user);
39+
await _unitOfWork.SaveChangesAsync(cancellationToken);
3640

37-
logger.LogInformation("Updated profile for user {UserId}", user.Id.Value);
41+
_logger.LogInformation("Updated profile for user {UserId}", user.Id.Value);
3842

39-
return mapper.Map<UserDto>(user);
43+
return _mapper.Map<UserDto>(user);
4044
}
4145
}

src/services/TeslaStarter.Application/Users/Queries/GetUser/GetUserQueryHandler.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ public sealed class GetUserQueryHandler(
77
IUserRepository userRepository,
88
IMapper mapper) : IRequestHandler<GetUserQuery, UserDto>
99
{
10+
private readonly IUserRepository _userRepository = userRepository;
11+
private readonly IMapper _mapper = mapper;
1012
public async Task<UserDto> Handle(GetUserQuery request, CancellationToken cancellationToken)
1113
{
12-
User user = await userRepository.GetByIdAsync(
14+
User user = await _userRepository.GetByIdAsync(
1315
new UserId(request.UserId),
1416
cancellationToken) ?? throw new NotFoundException(nameof(User), request.UserId);
1517

16-
return mapper.Map<UserDto>(user);
18+
return _mapper.Map<UserDto>(user);
1719
}
1820
}

src/services/TeslaStarter.Application/Users/Queries/GetUserByExternalId/GetUserByExternalIdQueryHandler.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ public sealed class GetUserByExternalIdQueryHandler(
66
IUserRepository userRepository,
77
IMapper mapper) : IRequestHandler<GetUserByExternalIdQuery, UserDto?>
88
{
9+
private readonly IUserRepository _userRepository = userRepository;
10+
private readonly IMapper _mapper = mapper;
911
public async Task<UserDto?> Handle(GetUserByExternalIdQuery request, CancellationToken cancellationToken)
1012
{
11-
User? user = await userRepository.GetByExternalIdAsync(
13+
User? user = await _userRepository.GetByExternalIdAsync(
1214
ExternalId.Create(request.ExternalId),
1315
cancellationToken);
1416

15-
return user != null ? mapper.Map<UserDto>(user) : null;
17+
return user != null ? _mapper.Map<UserDto>(user) : null;
1618
}
1719
}

src/services/TeslaStarter.Application/Vehicles/Commands/LinkVehicle/LinkVehicleCommandHandler.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ public sealed class LinkVehicleCommandHandler(
88
IMapper mapper,
99
ILogger<LinkVehicleCommandHandler> logger) : IRequestHandler<LinkVehicleCommand, VehicleDto>
1010
{
11+
private readonly IVehicleRepository _vehicleRepository = vehicleRepository;
12+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
13+
private readonly IMapper _mapper = mapper;
14+
private readonly ILogger<LinkVehicleCommandHandler> _logger = logger;
1115
public async Task<VehicleDto> Handle(LinkVehicleCommand request, CancellationToken cancellationToken)
1216
{
1317

1418
// Check if vehicle already exists with this identifier
15-
Vehicle? existingVehicle = await vehicleRepository.GetByVehicleIdentifierAsync(
19+
Vehicle? existingVehicle = await _vehicleRepository.GetByVehicleIdentifierAsync(
1620
request.VehicleIdentifier,
1721
cancellationToken);
1822

@@ -31,12 +35,12 @@ public async Task<VehicleDto> Handle(LinkVehicleCommand request, CancellationTok
3135
request.VehicleIdentifier,
3236
request.DisplayName);
3337

34-
vehicleRepository.Add(vehicle);
35-
await unitOfWork.SaveChangesAsync(cancellationToken);
38+
_vehicleRepository.Add(vehicle);
39+
await _unitOfWork.SaveChangesAsync(cancellationToken);
3640

37-
logger.LogInformation("Linked vehicle {VehicleIdentifier} to Tesla account {TeslaAccountId}",
41+
_logger.LogInformation("Linked vehicle {VehicleIdentifier} to Tesla account {TeslaAccountId}",
3842
vehicle.VehicleIdentifier, vehicle.TeslaAccountId.Value);
3943

40-
return mapper.Map<VehicleDto>(vehicle);
44+
return _mapper.Map<VehicleDto>(vehicle);
4145
}
4246
}

src/services/TeslaStarter.Application/Vehicles/Commands/SyncUserVehicles/SyncUserVehiclesCommandHandler.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ public sealed class SyncUserVehiclesCommandHandler(
1010
IUnitOfWork unitOfWork,
1111
ILogger<SyncUserVehiclesCommandHandler> logger) : IRequestHandler<SyncUserVehiclesCommand, int>
1212
{
13+
private readonly IUserRepository _userRepository = userRepository;
14+
private readonly IVehicleRepository _vehicleRepository = vehicleRepository;
15+
private readonly ITeslaApiService _teslaApiService = teslaApiService;
16+
private readonly IUnitOfWork _unitOfWork = unitOfWork;
17+
private readonly ILogger<SyncUserVehiclesCommandHandler> _logger = logger;
18+
1319
public async Task<int> Handle(SyncUserVehiclesCommand request, CancellationToken cancellationToken)
1420
{
15-
User? user = await userRepository.GetByExternalIdAsync(
21+
User? user = await _userRepository.GetByExternalIdAsync(
1622
ExternalId.Create(request.ExternalId),
1723
cancellationToken) ?? throw new NotFoundException(nameof(User), request.ExternalId);
1824

@@ -23,7 +29,7 @@ public async Task<int> Handle(SyncUserVehiclesCommand request, CancellationToken
2329
}
2430

2531
// Fetch vehicles from Tesla API
26-
IReadOnlyList<TeslaVehicleDto> teslaVehicles = await teslaApiService.GetVehiclesAsync(user.TeslaAccount.AccessToken!);
32+
IReadOnlyList<TeslaVehicleDto> teslaVehicles = await _teslaApiService.GetVehiclesAsync(user.TeslaAccount.AccessToken!);
2733

2834
_logger.LogInformation("Found {Count} vehicles for user {UserId}", teslaVehicles.Count, user.Id.Value);
2935

@@ -32,7 +38,7 @@ public async Task<int> Handle(SyncUserVehiclesCommand request, CancellationToken
3238
foreach (TeslaVehicleDto teslaVehicle in teslaVehicles)
3339
{
3440
// Check if vehicle already exists
35-
Vehicle? existingVehicle = await vehicleRepository.GetByVehicleIdentifierAsync(
41+
Vehicle? existingVehicle = await _vehicleRepository.GetByVehicleIdentifierAsync(
3642
teslaVehicle.Vin,
3743
cancellationToken);
3844

@@ -44,7 +50,7 @@ public async Task<int> Handle(SyncUserVehiclesCommand request, CancellationToken
4450
teslaVehicle.Vin,
4551
string.IsNullOrEmpty(teslaVehicle.DisplayName) ? null : teslaVehicle.DisplayName);
4652

47-
vehicleRepository.Add(vehicle);
53+
_vehicleRepository.Add(vehicle);
4854
syncedCount++;
4955

5056
_logger.LogInformation("Added new vehicle {VIN} for user {UserId}",
@@ -56,18 +62,16 @@ public async Task<int> Handle(SyncUserVehiclesCommand request, CancellationToken
5662
existingVehicle.UpdateDisplayName(teslaVehicle.DisplayName);
5763
existingVehicle.RecordSync();
5864

59-
vehicleRepository.Update(existingVehicle);
65+
_vehicleRepository.Update(existingVehicle);
6066
syncedCount++;
6167

6268
_logger.LogInformation("Updated vehicle {VIN} for user {UserId}",
6369
teslaVehicle.Vin, user.Id.Value);
6470
}
6571
}
6672

67-
await unitOfWork.SaveChangesAsync(cancellationToken);
73+
await _unitOfWork.SaveChangesAsync(cancellationToken);
6874

6975
return syncedCount;
7076
}
71-
72-
private readonly ILogger<SyncUserVehiclesCommandHandler> _logger = logger;
7377
}

0 commit comments

Comments
 (0)