在Marten中无法成功加载关联实体/文档的问题求助
问题背景
使用基于PostgreSQL的Marten文档数据库(版本7.26.0),查询MemberKycVerificationStep并加载关联的KycVerificationStep时触发空引用异常,异常栈信息如下:
System.ArgumentNullException: Value cannot be null. (Parameter 'key')
at System.Collections.Generic.Dictionary2.TryInsert(TKey key, TValue value, InsertionBehavior behavior) at System.Collections.Generic.Dictionary2.set_Item(TKey key, TValue value)
at Marten.Linq.Parsing.SelectParser.VisitConstant(ConstantExpression node)
...(省略后续栈信息)
涉及实体代码
KycVerificationStep 实体
public class KycVerificationStep : BaseEntity { public Guid Id { get; set; } public string Code { get; private set; } public string Name { get; set; } public string Description { get; private set; } public string Icon { get; private set; } public int Order { get; private set; } public DateTimeOffset DateCreated { get; private set; } public DateTimeOffset? DateUpdated { get; private set; } private KycVerificationStep() { } private KycVerificationStep( string name, string description, string icon, int order, IKycVerificationStepDomainService domainService) { CheckRule(new KycStepMustBeUniqueRule(domainService, name)); Name = name; Description = description; Icon = icon; Order = order; DateCreated = SystemClock.Now; Code = $"Step {Order}"; AddDomainEvent(new KycVerificationStepCreatedDomainEvent(Id)); } public static KycVerificationStep Create( string name, string description, string icon, int order, IKycVerificationStepDomainService domainService) { return new KycVerificationStep(name, description, icon, order, domainService); } // shortened for brevity }
MemberKycVerificationStep 实体
public class MemberKycVerificationStep : BaseEntity { public string Id { get; private set; } public Guid KycVerificationStepId { get; private set; } public KycVerificationStep KycVerificationStep { get; private set; } public Guid MemberId { get; private set; } public string Status { get; private set; } public DateTimeOffset DateUpdated { get; private set; } private MemberKycVerificationStep() { } private MemberKycVerificationStep( Guid kycVerificationStepId, Guid memberId) { Id = $"{memberId}_{kycVerificationStepId}"; KycVerificationStepId = kycVerificationStepId; MemberId = memberId; Status = VerificationStatus.NotStarted.ToString(); AddDomainEvent(new MemberKycVerificationStepCreatedDomainEvent(MemberId, KycVerificationStepId)); } public static MemberKycVerificationStep Create( Guid kycVerificationStepId, Guid memberId) { return new MemberKycVerificationStep(kycVerificationStepId, memberId); } }
出错的查询代码
public async Task<Result<IPagedList<MemberKycVerificationStepDto>>> Handle(GetAllVerificationStepsByUserQuery request, CancellationToken cancellationToken) { KycVerificationStep included = null; var results = await _querySession .Query<MemberKycVerificationStep>() .Include<KycVerificationStep>(x => included = x) .On(x => x.KycVerificationStepId) .Where(x => x.MemberId == request.UserId) .Select(x => new MemberKycVerificationStepDto { KycVerificationStepId = x.KycVerificationStepId, Name = included != null ? included.Name : "", DateUpdated = x.DateUpdated, Status = "x.Status", MemberId = x.MemberId }) .ToPagedListAsync(request.PageIndex, request.PageSize, cancellationToken); return await Result<IPagedList<MemberKycVerificationStepDto>>.SuccessAsync(results); }
问题原因
你在查询中使用外部变量included接收Include的关联文档,并在Select投影中直接引用该变量。Marten解析LINQ表达式时,会将included视为初始值为null的常量表达式,内部处理时尝试将null作为键插入字典,触发ArgumentNullException。Marten的Include机制是将关联文档加载到会话缓存中,不能直接在投影表达式中通过外部变量引用。
另外,代码中Status = "x.Status"是笔误,应改为x.Status。
解决方案
提供三种可行的解决方式:
方式一:使用Marten跨文档Join查询
直接通过Join关联两个文档类型,一次性完成查询和投影:
public async Task<Result<IPagedList<MemberKycVerificationStepDto>>> Handle(GetAllVerificationStepsByUserQuery request, CancellationToken cancellationToken) { var results = await _querySession .Query<MemberKycVerificationStep>() .Join( _querySession.Query<KycVerificationStep>(), memberStep => memberStep.KycVerificationStepId, kycStep => kycStep.Id, (memberStep, kycStep) => new MemberKycVerificationStepDto { KycVerificationStepId = memberStep.KycVerificationStepId, Name = kycStep?.Name ?? "", DateUpdated = memberStep.DateUpdated, Status = memberStep.Status, MemberId = memberStep.MemberId }) .Where(dto => dto.MemberId == request.UserId) .ToPagedListAsync(request.PageIndex, request.PageSize, cancellationToken); return await Result<IPagedList<MemberKycVerificationStepDto>>.SuccessAsync(results); }
方式二:先分页查询,再批量加载关联文档
先获取分页后的MemberKycVerificationStep,再批量加载关联的KycVerificationStep,最后手动映射DTO:
public async Task<Result<IPagedList<MemberKycVerificationStepDto>>> Handle(GetAllVerificationStepsByUserQuery request, CancellationToken cancellationToken) { // 分页查询MemberKycVerificationStep var memberSteps = await _querySession .Query<MemberKycVerificationStep>() .Where(x => x.MemberId == request.UserId) .ToPagedListAsync(request.PageIndex, request.PageSize, cancellationToken); // 提取所有关联ID,批量加载关联文档 var stepIds = memberSteps.Select(x => x.KycVerificationStepId).ToList(); var kycSteps = await _querySession.LoadManyAsync<KycVerificationStep>(stepIds, cancellationToken); // 映射DTO并保留分页信息 var dtoList = memberSteps.Select(m => new MemberKycVerificationStepDto { KycVerificationStepId = m.KycVerificationStepId, Name = kycSteps.TryGetValue(m.KycVerificationStepId, out var step) ? step.Name : "", DateUpdated = m.DateUpdated, Status = m.Status, MemberId = m.MemberId }).ToList(); var pagedDto = new PagedList<MemberKycVerificationStepDto>( dtoList, memberSteps.PageIndex, memberSteps.PageSize, memberSteps.TotalCount); return await Result<IPagedList<MemberKycVerificationStepDto>>.SuccessAsync(pagedDto); }
方式三:利用Marten会话缓存加载关联文档
先通过Include将关联文档加载到会话缓存,再从缓存中获取关联文档映射DTO:
public async Task<Result<IPagedList<MemberKycVerificationStepDto>>> Handle(GetAllVerificationStepsByUserQuery request, CancellationToken cancellationToken) { // 查询并Include关联文档到会话缓存 var memberSteps = await _querySession .Query<MemberKycVerificationStep>() .Include<KycVerificationStep>(x => x.KycVerificationStepId) .Where(x => x.MemberId == request.UserId) .ToPagedListAsync(request.PageIndex, request.PageSize, cancellationToken); // 从会话缓存中获取关联文档并映射DTO var dtoList = memberSteps.Select(m => { var kycStep = _querySession.Load<KycVerificationStep>(m.KycVerificationStepId); return new MemberKycVerificationStepDto { KycVerificationStepId = m.KycVerificationStepId, Name = kycStep?.Name ?? "", DateUpdated = m.DateUpdated, Status = m.Status, MemberId = m.MemberId }; }).ToList(); var pagedDto = new PagedList<MemberKycVerificationStepDto>( dtoList, memberSteps.PageIndex, memberSteps.PageSize, memberSteps.TotalCount); return await Result<IPagedList<MemberKycVerificationStepDto>>.SuccessAsync(pagedDto); }
内容的提问来源于stack exchange,提问作者Kacey Ezerioha

