0

我即將結束一個項目,我試圖使用DDD,但發現了一個明顯的錯誤,我不確定如何輕鬆解決。重構實體方法以避免併發性問題

這裏是我的實體 - 我已經減少了它的簡單:在SetUploadResult方法發生

public class Contribution : Entity 
{ 
    protected Contribution() 
    { 
     this.Parts = new List<ContributionPart>(); 
    } 

    internal Contribution(Guid id) 
    { 
     this.Id = id; 
     this.Parts = new List<ContributionPart>(); 
    } 

    public Guid Id { get; private set; } 

    protected virtual IList<ContributionPart> Parts { get; private set; } 

    public void UploadParts(string path, IEnumerable<long> partLengths) 
    { 
     if (this.Parts.Count > 0) 
     { 
      throw new InvalidOperationException("Parts have already been uploaded."); 
     } 

     long startPosition = 0; 
     int partNumber = 1; 

     foreach (long partLength in partLengths) 
     { 
      this.Parts.Add(new ContributionPart(this.Id, partNumber, partLength)); 
      this.Commands.Add(new UploadContributionPartCommand(this.Id, partNumber, path, startPosition, partLength)); 
      startPosition += partLength; 
      partNumber++; 
     } 
    } 

    public void SetUploadResult(int partNumber, string etag) 
    { 
     if (etag == null) 
     { 
      throw new ArgumentNullException(nameof(etag)); 
     } 

     ContributionPart part = this.Parts.SingleOrDefault(p => p.PartNumber == partNumber); 

     if (part == null) 
     { 
      throw new ContributionPartNotFoundException(this.Id, partNumber); 
     } 

     part.SetUploadResult(etag); 

     if (this.Parts.All(p => p.IsUploaded)) 
     { 
      IEnumerable<PartUploadedResult> results = this.Parts.Select(p => new PartUploadedResult(p.PartNumber, p.ETag)); 
      this.Events.Add(new ContributionUploaded(this.Id, results)); 
     } 
    } 
} 

我的錯誤。基本上,多個線程同時執行上載,然後在上傳結束時調用SetUploadResult。但是因爲實體預先加載了幾秒鐘,所以每個線程都將在實體的不同實例上調用SetUploadResult,因此測試if (this.Parts.All(p => p.IsUploaded)永遠不會評估爲true。

我不確定如何輕鬆解決此問題。將多個UploadContributionPartCommands添加到Commands集合的想法是,每個ContributionPart都可以並行上傳 - 我的CommandBus確保這一點 - 但是每個部分並行上傳,這會對我的實體邏輯造成問題。

+0

那麼您是說有多個線程在貢獻實體的同一個實例上運行? – mm8

+0

正確。貢獻實體爲每個partLength創建了UploadContributionPartCommand,並且每個UploadContributionPartCommandHandler並行執行,因此並行調用SetUploadResult。除了它與實體的內存實例不同,但它是同一個實體。 –

+1

由於每個實例都有自己的部分,它們怎麼會與「內存中的實例」不一樣? – mm8

回答

0

如果多個線程可能同時調用SetUploadResult方法,並且您有競爭條件,則應使用同步機制(如鎖:https://msdn.microsoft.com/en-us/library/c5kehkcz.aspx)來保護關鍵部分。

如果您鎖定字段static它會在你的實體類型的所有實例共享,例如:

private static readonly object _lock = new object(); 
public void SetUploadResult(int partNumber, string etag) 
{ 
    if (etag == null) 
    { 
     throw new ArgumentNullException(nameof(etag)); 
    } 

    ContributionPart part = this.Parts.SingleOrDefault(p => p.PartNumber == partNumber); 

    if (part == null) 
    { 
     throw new ContributionPartNotFoundException(this.Id, partNumber); 
    } 

    part.SetUploadResult(etag); 

    lock (_lock) //Only one thread at a time can enter this critical section. 
       //The second thread will wait here until the first thread leaves the critical section. 
    { 
     if (this.Parts.All(p => p.IsUploaded)) 
     { 
      IEnumerable<PartUploadedResult> results = this.Parts.Select(p => new PartUploadedResult(p.PartNumber, p.ETag)); 
      this.Events.Add(new ContributionUploaded(this.Id, results)); 
     } 
    } 
} 
1

我想你可以重構Contribution所以它不會處理SetUploadResult。它將使貢獻實體解耦,並且SetUploadResult的副作用被隔離,從而使技術問題脫離Contribution域模型。

創建一個調度程序類,其中包含SetUploadResult正在做的事情。

一旦Contribution實體完成其邏輯,執行線程將返回到應用程序服務。在這一點上,來自實體的事件可以被送入調度員。

如果它們是長時間運行的進程,則可以將它們添加爲任務的集合並異步運行它們。那麼你可以等待所有任務完成。你可以在SO中搜索如何做到這一點。

var results = await Task.WhenAll(task1, task2,...taskN); 
+1

同意。跟蹤上傳進度絕對是應用程序級別關注的問題,而不是域名問題。 – guillaume31

+0

公平評論。我們圍繞貢獻和貢獻的部分進行了很多討論,我們決定讓它們成爲領域的一部分,但有理由質疑它們是否應該如此。 –