无法跟踪实体实例,因为已在跟踪另一个具有相同键值的实体实例。 DDD + CQRS + EF Core

问题描述 投票:0回答:1

我或多或少遵循 Microsoft 的 DDD 和 CQRS 模式示例(eshopOnContainers 应用程序)来构建我的应用程序。

我有以下主要组件:

  1. 应用层/交易后服务
  2. 应用层/账单分类查询
  3. 领域模型层/模型类
  4. 存储库层/事务存储库(EF Core)

应用层/PostTransactionService:

在这里,我构建域模型对象,这些对象通常需要来自“BillingClassificationQueries”的域模型实例,例如,要构建“Transaction”实例,我需要一个如下所示的“transactionBillClassification”实例:

var transactionBillClassification = await _billingClassificationQueries.GetBillClassification(command.BillingClassificationId.Value);

var transaction = Transaction.CreateTransactionForChargeOrCredit(
                    account,
                    command.TransactionDate,
                    Enumeration.FromValue<ChargeAction>(command.ChargeActionId),
                    Enumeration.FromValue<ChargeType>(command.ChargeTypeId),
                    new Money(command.AmountPosted),                   
                    transactionBillClassification,
                    parentTransaction);

应用层/账单分类查询

现在,在 BillingClassificationQueries 类中,我仅使用 Dapper 从数据库读取数据,并将结果直接映射到域模型中(我知道,我可以使用视图模型,但我觉得我会遇到同样的问题),如下所示:

 public async Task<BillClassification> GetBillClassification(int billingClassificationId)
    {   
        using (var connection = new SqlConnection(_connectionString))
        {
            connection.Open();

            var query = $@"SELECT 
                        BillClassificationId
                        , BC.Name
                        , BC.ChargeType
                        , MA.MainAccountNo
                        , MA.Name as MainAccountName
                        , MA.MainAccountTypeId
                        , MA.MainAccountSubTypeId
                        , MA.WriteOffAccountNo
                    FROM [RATES].[BillClassification] BC
                    JOIN [ACCOUNTING].[MainAccount] MA
                        ON MA.MainAccountNo = BC.MainAccount                        
                    WHERE BillClassificationId = @billingClassificationId ";

            var result = await connection.QueryAsync<dynamic, dynamic, dynamic>(
                query
                , (billClassification, mainAccount) =>
                {
                    var mainAccountForBillClass = new MainAccount(mainAccount.MainAccountNo, mainAccount.MainAccountName, Enumeration.FromValue<MainAccountType>(mainAccount.MainAccountTypeId), Enumeration.FromValue<MainAccountSubType>(mainAccount.MainAccountSubTypeId), mainAccount.WriteOffAccountNo);
                    _transactionRepository.Detach(mainAccountForBillClass);

                    var billClass = new BillClassification(
                        billClassification.BillClassificationId,
                        billClassification.Name,
                        Enumeration.FromValue<ChargeType>(billClassification.ChargeType),
                        mainAccountForBillClass
                    );
                    _transactionRepository.Detach(billClass);

                    return billClass;
                }
                , new { billingClassificationId }, splitOn: "MainAccountNo");

            var billClassification = result.FirstOrDefault();

            return billClassification;
        }

请注意,我必须手动创建“MainAccount”实例来组成“BillClassification”实例。

应用层/PostTransactionService

同样,稍后我需要将“LineItem”实例添加到我的“Transaction”实例中,对于每个行项目,我需要使用“BillingClassificationQueries”提供的数据。

 foreach (var item in command.TransactionLineItems)
        {
            var itemBillingCode = await _billingClassificationQueries.GetBillingCode(item.BillingCodeId.Value);
            //_transactionRepository.Detach(itemBillingCode);               

            var lineItem = TransactionLineItem.CreateLineItemForChargeOrCredit(
                transaction,
                account.AccountNumber,
                command.TransactionDate,
                new Money(item.AmountPosted),
                new Money(command.ArBalance),
                "",
                itemBillingCode);

            transaction.AddLineItem(lineItem);
        }

 请注意“itemBillingCode”的使用,它来自 BillingClassificationQueries,我按如下方式检索其数据:

应用层/账单分类查询

 var result = await connection.QueryAsync<dynamic, dynamic, dynamic, dynamic, dynamic>(
                 query
                 ,(billingCode, billingCodeMainAccount, billClassification, billClassificationMainAccount) => 
                 {
                     var mainAccountForBillingClass = new MainAccount(
                                 billClassificationMainAccount.BC_MA_MainAccountNo,
                                 billClassificationMainAccount.BC_MA_Name,
                                 Enumeration.FromValue<MainAccountType>(billClassificationMainAccount.BC_MA_MainAccountTypeId),
                                 Enumeration.FromValue<MainAccountSubType>(billClassificationMainAccount.BC_MA_MainAccountSubTypeId),
                                 billClassificationMainAccount.BC_MA_WriteOffAccountNo
                                 );
                     //_transactionRepository.Detach(mainAccountForBillingClass);

                     var mainAccountForBillingCode = new MainAccount(
                                 billingCodeMainAccount.BCD_MA_MainAccountNo,
                                 billingCodeMainAccount.BCD_MA_Name,
                                 Enumeration.FromValue<MainAccountType>(billingCodeMainAccount.BCD_MA_MainAccountTypeId),
                                 Enumeration.FromValue<MainAccountSubType>(billingCodeMainAccount.BCD_MA_MainAccountSubTypeId),
                                 billingCodeMainAccount.BCD_MA_WriteOffAccountNo
                                 );
                    // _transactionRepository.Detach(mainAccountForBillingCode);

                     var result = new BillingCode(
                         billingCode.BCD_BillingCodeId,
                         billingCode.BCD_Name,
                         new BillClassification(
                             billClassification.BC_BillClassificationId,
                             billClassification.BC_Name,
                             Enumeration.FromValue<ChargeType>(billClassification.BC_ChargeTypeId),
                             mainAccountForBillingClass
                             ),
                         mainAccountForBillingCode,
                         new Money(billingCode.BCD_SuggestedAmount)
                         );
                     //_transactionRepository.Detach(result);

                     return result;
                 }
                , new { billingCodeId }, splitOn: "BCD_MA_MainAccountNo, BC_BillClassificationId, BC_MA_MainAccountNo"
                );

再次注意 MainAccount 实例“mainAccountForBillingClass”的存在,它将与前面讨论的 MainAccount 实例具有相同的 ID (MainAccountNo)。

说到我的问题,当我致电

_context.Add(transaction).Entity;
时,我收到以下错误:

无法跟踪实体类型“MainAccount”的实例,因为已跟踪具有相同键值 {'MainAccountNo'} 的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值

我猜测问题是第一个查询中的两个 MainAccount 实例“mainAccountForBillClass”和第二个查询中的“mainAccountForBillingClass”。正如您可能在一些注释行中看到的那样,我尝试通过以下方式从 DBContext 中分离这些实例,但仍然遇到相同的问题:

public void Detach(Entity entity)
    {
        _context.Entry(entity).State = EntityState.Detached;
    }

最后我的 Repo 和 DBContext 都注册为 Scoped(默认生活方式)

builder.Services.AddScoped<ITransactionRepository, TransactionRepository>();
builder.Services.AddScoped<IBillingClassificationQueries>(sp => new BillingClassificationQueries(builder.Configuration["ConnectionString"].ToString(), sp.GetRequiredService<ITransactionRepository>()));

关于如何处理这个问题有什么建议吗?

.net entity-framework domain-driven-design cqrs
1个回答
0
投票

查询端不应有任何对存储库接口的引用。为查询端创建一个新的 QueryDbContext 并在其构造函数中将其设置为只读:

ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

CQRS 已经将查询和命令的整个过程分开。在理想的实现中,您的读写模型必须不同,并且不能相互引用,并且它们必须存储在不同的存储中。因此,如果您使用的是 CQRS 的弱版本,至少您应该为它们每个创建一个新的 DbContext 并将查询 DbContext 设为只读。这样,您就可以放心,任何修改都不会通过 QueryDbContext 影响系统。

© www.soinside.com 2019 - 2024. All rights reserved.