entity-framework-core - 我应该在相关聚合根之间强制执行引用完整性吗?如果是的话,在哪里?

标签 entity-framework-core domain-driven-design abp

这似乎是一个基本问题,而且我对 DDD 的研究不多。这个问题的背景是关于使用 ABP 框架、它的层和它生成的代码。

我应该强制相关聚合根之间的引用完整性吗?如果是的话,在哪里?

在使用 ABP Suite 生成我的初始实体(其中许多是聚合根(AR))之后,我开始实现它们之间的导航属性。我最初的方法是修改每个实体/AR 的构造函数以包含依赖实体/AR 的 Guid ID。

原始方法

public Address(Guid id, string name, AddressTypes addressType,
    string line1, string line2, string line3,
    string city, string postalCode, 
    Guid stateId, Guid countryId, // <-- dependent AR IDs
    bool primary = false, bool active = true)
{
    //other properties set
    StateId = stateId;
    CountryId = countryId;
    //etc
}

我让我的域数据种子逻辑与我的数据种子贡献者一起工作,然后我继续处理测试数据。

很快我意识到我必须创建每个依赖实体/AR 并将它们的 ID 传递给每个被测试实体的构造函数。

这促使我在文档中搜索示例或最佳实践。我遇到的一件事是来自 Entity Best Practices & Conventions 的声明页面:

Do always reference to other aggregate roots by Id. Never add navigation properties to other aggregate roots.

好的。因此,这似乎表明我应该在每个从属/子 AR 的主体/父 AR 中具有可为空的 ID 属性。 AR 下的完全托管实体可能可以自由地拥有导航属性,但 AR 则不然。

因为我想在更高级别管理 AR 关系,所以我似乎应该从构造函数中删除依赖的 AR,或者至少使它们可以为空。

修订方法 1

public Address(Guid id, string name, AddressTypes addressType,
    string line1, string line2, string line3, 
    string city, string postalCode, 
    Guid? stateId = null, Guid? countryId = null, // <-- nullable dependent AR IDs
    bool primary = false, bool active = true)
{
    //other properties set
    StateId = stateId;
    CountryId = countryId;
    //etc
}

修改后的方法 2

public Address(Guid id, string name, AddressTypes addressType,
    string line1, string line2, string line3, 
    string city, string postalCode, 
    bool primary = false, bool active = true)
{
    //other properties set, not setting dependent IDs
}

显然修改后的方法 2 需要应用程序服务在构造对象后设置依赖 ID 属性:

Address address = null;
address = await _addressRepository.InsertAsync(new Address
(
    id: _guidGenerator.Create(),
    name: "DEMO Contact Home Address",
    addressType: AddressTypes.Home,
    line1: "123 Main St",
    line2: "",
    line3: "",
    city: "Anytown",
    postalCode: "00000",
    primary: true,
    active: true
), autoSave: true);
address.StateId = alState.Id; // set here
address.CountryId = usId; // and here

到目前为止我是否步入正轨?

因此,如果我想强制引用完整性,我不应该通过 Entity Framework (或选择的 ORM)来做到这一点。我应该在应用程序服务层强制引用完整性。

[Authorize(MyProductPermissions.Addresses.Create)]
public virtual async Task<AddressDto> CreateAsync(AddressCreateDto input)
{
    if (input.StateId == default)
    {
        throw new UserFriendlyException(L["The {0} field is required.", L["State"]]);
    }
    if (input.CountryId == default)
    {
        throw new UserFriendlyException(L["The {0} field is required.", L["Country"]]);
    }

    var address = ObjectMapper.Map<AddressCreateDto, Address>(input);
    address.TenantId = CurrentTenant.Id;
    
    // BEGIN Referential Integrity Logic 

    // Assumes that the dependent IDs will be set when the DTO is mapped to the entity
    if (address.StateId == null)
    {
        // log and throw 500 error
    }
    if (address.CountryId == null)
    {
        // log and throw 500 error
    }

    // END Referential Integrity Logic
    
    address = await _addressRepository.InsertAsync(address, autoSave: true);
    return ObjectMapper.Map<Address, AddressDto>(address);
}

这样的理解正确吗?

如果我的依赖 ID 可为空,我可以继续使用 ABP Suite 生成的测试代码。

await _addressRepository.InsertAsync(new Address
(
    Guid.Parse("ca846f1a-8bbd-4e2c-afbd-8e40a03ae18f"),
    "7d7b348e410d48ee89e1807beb2f2ac0bd66af4ea82943ec8eee3a52962577b1",
    default,
    "de5ec0226aba4c1a837c9716b21af6551d10436756724d4fa507028eaaddcdadec779bea0ef04922992f9d2432068b180e6fe95f425f47c68559c1dbd4360fdb",
    "53bc12edeb4544158147f3b835b0c4ce5e581844f5c248d69647d80d398706f5ee1c769e4ee14bd0a1e776a369a96ea3c0582b659ce342bdbdf40e6668f3b9f9",
    "117880188dfd4a6f96892fea3e62a16f057748ebe76b4dd0a4402918e2fee9055272ff81c53d4c28825cc20d01918386864efd54e1aa458bb449a1d12b349d40",
    "866a81007219411a971be2133bf4b5882d4ef612722a45ac91420e0b30d774ed",
    "93bba338449444f5",
    true,
    true
));

如果不是,我将必须增强测试代码,为与我的 AR 关联的每个依赖实体类型创建至少一个依赖实体。

// Create dependent State entity
var alState = //...

// Create dependent Country entity
var country = //...

Address address = null;
address = await _addressRepository.InsertAsync(new Address
(
    Guid.Parse("ca846f1a-8bbd-4e2c-afbd-8e40a03ae18f"),
    "7d7b348e410d48ee89e1807beb2f2ac0bd66af4ea82943ec8eee3a52962577b1",
    default,
    "de5ec0226aba4c1a837c9716b21af6551d10436756724d4fa507028eaaddcdadec779bea0ef04922992f9d2432068b180e6fe95f425f47c68559c1dbd4360fdb",
    "53bc12edeb4544158147f3b835b0c4ce5e581844f5c248d69647d80d398706f5ee1c769e4ee14bd0a1e776a369a96ea3c0582b659ce342bdbdf40e6668f3b9f9",
    "117880188dfd4a6f96892fea3e62a16f057748ebe76b4dd0a4402918e2fee9055272ff81c53d4c28825cc20d01918386864efd54e1aa458bb449a1d12b349d40",
    "866a81007219411a971be2133bf4b5882d4ef612722a45ac91420e0b30d774ed",
    "93bba338449444f5",
    true,
    true
));
address.StateId = state.Id;
address.CountryId = country.Id;

这可能会成为我的层次结构中的许多对象,该层次结构目前约有 30 个实体/AR。多级依赖关系加剧了这种情况。

请帮助我了解 DDD 世界中的最佳实践。在开始实现 30 个奇怪的构造函数和应用程序服务之前,我需要先解决这个问题。

最佳答案

如果您的 Address 实体必须使用指定的 StateIdCountryId 创建,您需要使用原始方法并强制设置它们的值,同时对象创建。因为,聚合根有责任维护其自身的完整性。 (参见相关 documentation了解更多信息)

  • 我猜您还会问如果您的数据库中不存在 StateId 并且它只是一个简单的 GUID,会发生什么情况。在这种情况下,如果您将 StateId 设置为外键,它将不会添加到您的数据库中。但是,如果您想以任何方式查询它并在不存在时抛出异常,您可以创建一个域服务并检查是否存在具有给定 stateId 的状态,如果存在则将其传递给 Address 构造函数(如果不存在)抛出异常)并在数据库中创建新的地址记录。
public class AddressManager : DomainService 
{
   private readonly IAddressRepository _addressRepository;
   private readonly IStateRepository _stateRepository;
   private readonly ICountryRepository _countryRepository;

   public AddressManager(IAddressRepository addressRepository, IStateRepository stateRepository, ICountryRepository countryRepository)
   {
      _addressRepository = addressRepository;
      _stateRepository = stateRepository;
      _countryRepository = countryRepository;
   }


   public async Task CreateAsync(string name, AddressTypes addressType,
    string line1, string line2, string line3,
    string city, string postalCode, 
    Guid stateId, Guid countryId)
   {
      if(await _stateRepository.FindAsync(stateId))
      {
         //throw exception
         return;
      }

      if(await _countryRepository.FindAsync(stateId))
      {
         //throw exception
         return;
      }

      var address = new Address(GuidGenerator.Create(), AddressTypes.Typee, "line1", "line2", "line3", "city", "postalCode", stateId, countryId);

      await _addressRepository.InsertAsync(address);
   }
}

创建新地址时,请在应用服务中调用 AddressManager 的 CreateAsync 方法。 (您可能希望将 Address 实体构造函数设置为内部而不是公共(public),以防止在应用程序层错误地创建 Address 对象。)

关于entity-framework-core - 我应该在相关聚合根之间强制执行引用完整性吗?如果是的话,在哪里?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69980421/

相关文章:

ubuntu - 如何将 ABP 框架应用程序部署到 Ubuntu 服务器?

abp - 如何将包含数据的模型从数据库传递到 ABP.IO 布局 Hook ?

c# - 自动递增数字的最佳实践 EF Core

c# - 如何使用“Entity Framework Core”(又名EF7)实现“软删除”?

c# - EF Core 5 - 如何将 EF.Functions.Like 与映射到 JSON 字符串的自定义属性一起使用?

c# - Entity Framework 7 RC 1 和 ASP.NET MVC 6 中的种子初始数据

domain-driven-design - 事件溯源 - 域逻辑适用于何处?

unit-testing - UNIT 测试 dto 和域对象

repository - DDD : Do item counts belong in domain model?

abp - ABP MVC项目如何从Lepton Theme切换到Basic Theme