Home > other >  ProjectTo - Value cannot be null . (Parameter 'source')
ProjectTo - Value cannot be null . (Parameter 'source')

Time:07-02

The following code leads to a Value cannot be null. (Parameter 'source') exception. It is caused by .ProjectTo and more specifically it is caused by DisciplineHours in DisciplineDto. It's probably hard to tell what's causing it, but how do I trace it back?

It's probably smt like .ThenInclude(discipline => discipline.DisciplineHours).

fail: AcademicSchedule.Application.Disciplines.Queries.GetDisciplinesByFlowIdQuery[0] Request: Unhandled Exception for Request GetDisciplinesByFlowIdQuery AcademicSchedule.Application.Disciplines.Queries.GetDisciplinesByFlowIdQuery System.ArgumentNullException: Value cannot be null. (Parameter 'source') at System.Linq.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument) at System.Linq.Enumerable.Select[TSource,TResult](IEnumerable1 source, Func2 selector) at lambda_method274(Closure , Discipline ) at System.Linq.Enumerable.SelectListIterator2.MoveNext() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at System.Linq.Enumerable.ToList[TSource](IEnumerable1 source)

using Response = List<DisciplineDto>;

public class GetDisciplinesByFlowIdQuery : IRequest<Response>
{
    public int FlowId { get; set; }
}

public class GetDisciplinesByFlowIdQueryHandler : IRequestHandler<GetDisciplinesByFlowIdQuery, Response>
{
    private readonly IApplicationDbContext _context;
    private readonly IMapper _mapper;

    public GetDisciplinesByFlowIdQueryHandler(IApplicationDbContext context, IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    public Task<Response> Handle(GetDisciplinesByFlowIdQuery request, CancellationToken cancellationToken)
    {
        var flow = _context.Flows
            .Include(f => f.Disciplines)
            .SingleOrDefault(x => x.Id == request.FlowId);

        if (flow == null)
        {
            throw new NotFoundException(nameof(Flow), request.FlowId);
        }

        var result = flow.Disciplines
            .AsQueryable()
            .ProjectTo<DisciplineDto>(_mapper.ConfigurationProvider)
            .ToList();

        return Task.FromResult(result);
    }
}
public class DisciplineDto : IMapFrom<Discipline>
{
    public int Id { get; set; }

    public string ShortName { get; set; }

    public string FullName { get; set; }

    public string DisciplineCode { get; set; }

    public int SemesterNumber { get; set; }

    public bool IsRequired { get; set; }

    public string CurriculumCode { get; set; }

    public int CurriculumVersion { get; set; }

    public DepartmentDto Department { get; set; }
    public PeriodDto Period { get; set; }
    public List<DisciplineHourDto> DisciplineHours { get; set; } // Caused by this line
}

public class Discipline
{
    public int Id { get; set; }
    public string ShortName { get; set; }
    public string FullName { get; set; }
    public string DisciplineCode { get; set; }
    public int SemesterNumber { get; set; }
    public bool IsRequired { get; set; }
    public string CurriculumCode { get; set; }
    public int CurriculumVersion { get; set; }

    public int DepartmentId { get; set; }
    public Department Department { get; set; }
    
    public int PeriodId { get; set; }
    public Period Period { get; set; }

    public List<Flow> Flows { get; set; }
    public List<DisciplineHour> DisciplineHours { get; set; }
    public List<ClassActivity> ClassActivities { get; set; }
}

public class Flow
{
    public int Id { get; set; }
    public int PeriodId { get; set; }
    public Period Period { get; set; }

    public List<Discipline> Disciplines { get; set; }
    public List<Subgroup> Subgroups { get; set; }
    public List<ClassActivity> ClassActivities { get; set; }
}

public class DisciplineHour
{
    public int DisciplineId { get; set; }
    public Discipline Discipline { get; set; }

    public int ClassActivityTypeId { get; set; }
    public ClassActivityType ClassActivityType { get; set; }

    public decimal Hours { get; set; }
}

public class DisciplineHourDto : IMapFrom<DisciplineHour>
{
    public int DisciplineId { get; set; }

    public int ClassActivityTypeId { get; set; }

    public decimal Hours { get; set; }

    public string DisciplineName { get; set; }

    public string ClassActivityTypeName { get; set; }

    public string FullName { get; set; }

    public void Mapping(Profile profile)
    {
        profile.CreateMap<DisciplineHour, DisciplineHourDto>()
            .ForMember(dto => dto.DisciplineName, opt => opt.MapFrom(sh => sh.Discipline.FullName))
            .ForMember(dto => dto.ClassActivityTypeName, opt => opt.MapFrom(sh => sh.ClassActivityType.ShortName))
            .ForMember(dto => dto.FullName, opt => opt.MapFrom(sh => $"{sh.ClassActivityType.FullName} {sh.Discipline.ShortName}"));
    }
}

CodePudding user response:

Your error appears because your SQL query doesn't load the Discipline.DisciplineHours property and EF Core doesn't lazy-load the property for you on demand.

When working with automapper you have to decide whether you want to project on the sql query level or map in memory after all data are retrieved from the database.

Writing the projection for the sql query becomes easier when you start with the root entity of your projection. So, instead of starting your query with _context.Flows, start your query with _context.Disciplines and find a condition that's logically equivalent to your original query condition. Project your query to the desired DTO before materializing the data like

var result = _context.Disciplines
    .Where(d => d.Flows.Any(f => f.Id == request.FlowId))
    .ProjectTo<DisciplineDto>(_mapper.ConfigurationProvider)
    .ToList();

This only works when Automapper is configured in a way that doesn't require any client side functions. Otherwise you would need to load the whole model into memory first. This can be done with eager loading using _context.Flows.Include(x => x.Disciplines).ThenInclude(y => y.DisciplineHours)... until all required data are loaded.

If the object graph is to complex for complete eager loading, you can also partially load the data initially, and then load more parts in separate queries. Have a look at https://stackoverflow.com/a/40068336/5265292 for more details.

  • Related