领域实体Player设置ApplicationUser的UserId属性:私有Setter的解决方案咨询
Great question—this is a common scenario when pairing identity systems with domain-driven design (DDD) principles, and your instinct to avoid exposing the private setter is exactly right. Let’s break down your options and the best practices here.
Your Proposed Solution: Adding a SetApplicationUserId Method
First off: yes, adding a dedicated method to set the ApplicationUserId is a completely reasonable and encapsulation-friendly approach. This keeps the property’s setter private (preventing arbitrary external modifications) while explicitly defining a controlled way to set this value when needed—aligning with DDD’s focus on encapsulating entity behavior rather than just exposing raw data.
Here’s how you’d implement this in your Player class:
public class Player : MyEntity { public string UserName { get; private set; } public virtual Guid ApplicationUserId { get; private set; } private Player() { } public Player(string userName) { UserName = userName; // Optional: Add validation for userName (e.g., not null/empty) } public void SetApplicationUserId(Guid applicationUserId) { // Enforce business invariants to keep the entity valid if (applicationUserId == Guid.Empty) throw new ArgumentException("ApplicationUserId cannot be an empty GUID.", nameof(applicationUserId)); if (ApplicationUserId != Guid.Empty) throw new InvalidOperationException("ApplicationUserId has already been set and cannot be changed."); ApplicationUserId = applicationUserId; } }
Then, adjust your workflow to pass the ApplicationUserId from the UserService to the CreatePlayerCommand:
Update UserService to Pass the Created User’s ID
public async Task<(AppSignInResult result, SignInData data)> CreateUser(string username, string password, string email, string country) { var user = new ApplicationUser() { UserName = username, Email = email}; var result = await _userManager.CreateAsync(user, password); if (!result.Succeeded) { // Handle creation failure (e.g., return error result) return (AppSignInResult.Failed, null); } // Pass the newly created ApplicationUser's ID to the command var command = new CreatePlayerCommand(username, country, user.Id); var playerId = await _mediator.Send(command); // Rest of your logic to build the return data return (AppSignInResult.Success, new SignInData { PlayerId = playerId }); }
Update CreatePlayerCommand and Handler
// Command definition public class CreatePlayerCommand : IRequest<int> { public string UserName { get; } public string Country { get; } public Guid ApplicationUserId { get; } public CreatePlayerCommand(string userName, string country, Guid applicationUserId) { UserName = userName; Country = country; ApplicationUserId = applicationUserId; } } // Handler implementation public async Task<int> Handle(CreatePlayerCommand request, CancellationToken cancellationToken) { var player = new Player(request.UserName); player.SetApplicationUserId(request.ApplicationUserId); _unitOfWork.Players.Add(player); await _unitOfWork.SaveChanges(); return player.Id; }
A Better Alternative: Initialize ApplicationUserId in the Constructor
If ApplicationUserId is a required property for a valid Player entity (i.e., a player shouldn’t exist without being linked to an ApplicationUser), you should initialize it directly in the constructor. This enforces the entity’s invariants from the moment it’s created, eliminating the possibility of an invalid, unlinked player state.
Adjust the Player class:
public class Player : MyEntity { public string UserName { get; private set; } public virtual Guid ApplicationUserId { get; private set; } private Player() { } public Player(string userName, Guid applicationUserId) { // Enforce business rules upfront if (string.IsNullOrWhiteSpace(userName)) throw new ArgumentException("Username cannot be null or empty.", nameof(userName)); if (applicationUserId == Guid.Empty) throw new ArgumentException("ApplicationUserId cannot be an empty GUID.", nameof(applicationUserId)); UserName = userName; ApplicationUserId = applicationUserId; } }
Then simplify your handler:
public async Task<int> Handle(CreatePlayerCommand request, CancellationToken cancellationToken) { var player = new Player(request.UserName, request.ApplicationUserId); _unitOfWork.Players.Add(player); await _unitOfWork.SaveChanges(); return player.Id; }
Why Avoid Opening the Setter?
Directly making the ApplicationUserId setter public would break encapsulation. It allows any external code to modify this value at any time, potentially bypassing business rules (like preventing changes to an already linked user) and putting your entity into invalid states. Both the method-based and constructor-based approaches keep control over how and when this value is set.
内容的提问来源于stack exchange,提问作者hoozr

