基於ABP實現DDD--實體建立和更新

2022-07-24 15:00:38

  本文主要介紹了通過建構函式和領域服務建立實體2種方式,後者多用於在建立實體時需要其它業務規則檢測的場景。最後介紹了在應用服務層中如何進行實體的更新操作。

一.通過建構函式建立實體

假如Issue的聚合根類為:

public class Issue : AggregateRoot<Guid>
{
    public Guid RepositoryId { get; private set; } //不能修改RepositoryId,原因是不支援把一個Issue移動到另外一個Repository下面
    public string Title { get; private set; } //不能直接修改Title,可以通過SetTitle()修改,主要是在該方法中要加入對Title不能重複的校驗
    public string Text { get; set; } //可以直接修改
    public Guid? AssignedUserId { get; internal set; } //同一個程式集中是可以修改AssignedUserId的
    
    // 公有建構函式
    public Issue(Guid id, Guid repositoryId, string title, string text=null) : base(id)
    {
        RepositoryId = repositoryId;
        Title = Check.NotNullOrWhiteSpace(title, nameof(title));
        Text = text; //可為空或者null
    }
    
    // 私有建構函式
    private Issue() {}

    // 修改Title的方法
    public void SetTitle(string title)
    {
        // Title不能重複
        Title = Check.NotNullOrWhiteSpace(title, nameof(title));
    }
    // ...
}

在應用服務層建立一個Issue的過程如下:

public class IssueAppService : ApplicationService.IIssueAppService
{
    private readonly IssueManager _issueManager; //Issue領域服務
    private readonly IRepository<Issue, Guid> _issueRepository; //Issue倉儲
    private readonly IRepository<AppUser, Guid> _userRepository; //User倉儲
    
    // 公有建構函式
    public IssueAppService(IssueManager issueManager, IRepository<Issue, Guid> issueRepository, IRepository<AppUser, Guid> userRepository)
    {
        _issueManager = issueManager;
        _issueRepository = issueRepository;
        _userRepository = userRepository;
    }

    // 通過建構函式建立Issue
    public async Task<IssueDto> CreateAssync(IssueCreationDto input)
    {
        var issue = new Issue(GuidGenerator.Create(), input.RepositoryId, input.Title, input.Text);
    }
    
    if(input.AssigneeId.HasValue)
    {
        // 獲取分配給Issue的User
        var user = await _userRepository.GetAsync(input.AssigneeId.Value);
        // 通過Issue的領域服務,將Issue分配給User
        await _issueManager.AssignAsync(issue, user);
    }
    
    // 插入和更新Issue
    await _issueRepository.InsertAsync(issue);
    
    // 返回IssueDto
    return ObjectMapper.Map<Issue, IssueDto>(issue);
}

二.通過領域服務建立實體

  什麼樣的情況下會用領域服務建立實體,而不是通過實體建構函式來建立實體呢?主要用在建立實體時需要其它業務規則檢測的場景。比如,在建立Issue的時候,不能建立Title相同的Issue。通過Issue實體建構函式來建立Issue實體,這個是控制不住的。所以才會有通過領域服務建立實體的情況。
阻止從Issue的建構函式來建立Issue實體,需要將其建構函式的存取許可權由public修改為internal:

public class Issue : AggregateRoot<Guid>
{
    // ...

    internal Issue(Guid id, Guid repositoryId, string title, string text = null) : base(id)
    {
        RepositoryId = repositoryId;
        Title = Check.NotNullOrEmpty(title, nameof(title));
        Text = text; //允許為空或者null
    }
    
    // ...
}

通過領域服務IssueManager中的CreateAsync()方法來判斷建立的Issue的Title是否重複:

public class IssueManager:DomainService
{
    private readonly IRepository<Issue,Guid> _issueRepository; // Issue的倉儲
    
    // 公有建構函式,注入倉儲
    public IssueManager(IRepository<Issue,Guid> issueRepository)
    {
        _issueRepository=issueRepository;
    }
    
    public async Task<Issue> CreateAsync(Guid repositoryId, string title, string text=null)
    {
        // 判斷Issue的Title是否重複
        if(await _issueRepository.AnyAsync(i=>i.Title==title))
        {
            throw new BusinessException("IssueTracking:IssueWithSameTitleExists");
        }
        // 返回建立的Issue實體
        return new Issue(GuidGenerator.Create(), repositoryId, title, text);
    }
}

在應用服務層IssueAppService中通過IssueManager.CreateAsync()建立實體如下:

public class IssueAppService :ApplicationService.IIssueAppService
{
    private readonly IssueManager _issueManager; //Issue的領域服務
    private readonly IRepository<Issue,Guid> _issueRepository; //Issue的倉儲
    private readonly IRepository<AppUser,Guid> _userRepository; //User的倉儲
    
    // 公共的建構函式,注入所需的依賴
    public IssueAppService(IssueManager issueManager, IRepository<Issue,Guid> issueRepository, IRepository<AppUser,Guid> userRepository){
        _issueManager=issueManager;
        _issueRepository=issueRepository;
        _userRepository=userRepository;
    } 
    
    // 建立一個Issue
    public async Task<IssueDto> CreateAsync(IssueCreationDto input)
    {
        // 通過領域服務的_issueManager.CreateAsync()建立實體,主要是保證Title不重複
        var issue=await _issueManager.CreateAsync(input.RepositoryId, input.Title, input.Text);
        
        // 獲取User,並將Issue分配給User
        if(input.AssignedUserId.HasValue)
        {
            var user =await _userRepository.GetAsync(input.AssignedUserId.Value);
            await _issueManager.AssignToAsynce(issue,user);
        }
        // 插入和更新資料庫
        await _issueRepository.InsertAsync(issue);
        // 返回IssueDto
        return ObjectMapper.Map<Issue,IssueDto>(issue);
    }
}

// 定義Issue的建立DTO為IssueCreationDto
public class IssueCreationDto
{
    public Guid RepositoryId{get;set;}
    [Required]
    public string Title {get;set;}
    public Guid? AssignedUserId{get;set;}
    public string Text {get;set;}
}

現在有個疑問是為什麼不把Title的重複檢測放在領域服務層中來做呢,這就涉及一個區分核心領域邏輯還是應用邏輯的問題了。顯然這裡Title不能重複屬於核心領域邏輯,所以放在了領域服務中來處理。為什麼標題重複檢測不在應⽤服務中實現?詳細的解釋參考[1]。

三.實體的更新操作

接下來介紹在應用層IssueAppService中來update實體。定義UpdateIssueDto如下:

public class UpdateIssueDto
{
    [Required]
    public string Title {get;set;}
    public string Text{get;set;}
    public Guid? AssignedUserId{get;set;}
}

實體更新操作的UpdateAsync()方法如下所示:

public class IssueAppService :ApplicationService.IIssueAppService
{
    private readonly IssueManager _issueManager; //Issue領域服務
    private readonly IRepository<Issue,Guid> _issueRepository; //Issue倉儲
    private readonly IRepository<AppUser,Guid> _userRepository; //User倉儲
    
    // 公有建構函式,注入依賴
    public IssueAppService(IssueManager issueManager, IRepository<Issue,Guid> issueRepository, IRepository<AppUser,Guid> userRepository){
        _issueManager=issueManager;
        _issueRepository=issueRepository;
        _userRepository=userRepository;
    }
    
    // 更新Issue
    public async Task<IssueDto> UpdateAsync(Guid id, UpdateIssueDto input)
    {
        // 從Issue倉儲中獲取Issue實體
        var issue = await _issueRepository.GetAsync(id);
        
        // 通過領域服務的issueManager.ChangeTitleAsync()方法更新Issue的標題
        await _issueManager.ChangeTitleAsync(issue,input.Title);
        
        // 獲取User,並將Issue分配給User
        if(input.AssignedUserId.HasValue)
        {
            var user = await _userRepository.GetAsync(input.AssignedUserId.Value);
            await _issueManager.AssignToAsync(issue, user);
        }
        issue.Text=input.Text;
        // 更新和儲存Issue
        // 儲存實體更改是應用服務方法的職責
        await _issueRepository.UpdateAsync(issue);
        // 返回IssueDto
        return ObjectMapper.Map<Issue,IssueDto>(issue);
    }
}

需要在IssueManager中新增ChangeTitle():

public async Task ChangeTitleAsync(Issue issue,string title)
{
    // Title不變就返回
    if(issue.Title==title)
    {
        return;
    }
    // Title重複就丟擲異常
    if(await _issueRepository.AnyAsync(i=>i.Title==title))
    {
        throw new BusinessException("IssueTracking:IssueWithSameTitleExists");
    }
    // 請它情況更新Title
    issue.SetTitle(title);
}

修改Issue類中SetTitle()方法的存取許可權為internal:

internal void SetTitle(string title)
{
    Title=Check.NotNullOrWhiteSpace(title,nameof(title));
}

參考文獻:
[1]基於ABP Framework實現領域驅動設計:https://url39.ctfile.com/f/2501739-616007877-f3e258?p=2096 (存取密碼: 2096)