我或多或少遵循 Microsoft 的 DDD 和 CQRS 模式示例(eshopOnContainers 应用程序)来构建我的应用程序。
我有以下主要组件:
应用层/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>()));
关于如何处理这个问题有什么建议吗?
查询端不应有任何对存储库接口的引用。为查询端创建一个新的 QueryDbContext 并在其构造函数中将其设置为只读:
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
CQRS 已经将查询和命令的整个过程分开。在理想的实现中,您的读写模型必须不同,并且不能相互引用,并且它们必须存储在不同的存储中。因此,如果您使用的是 CQRS 的弱版本,至少您应该为它们每个创建一个新的 DbContext 并将查询 DbContext 设为只读。这样,您就可以放心,任何修改都不会通过 QueryDbContext 影响系统。