I got this simple API in .NET 8, added Mediatr and OData packages as well as AutoMapper
I’m trying not to expose my data layer stuff to the API layer, so everything needs to be done via mediator queries.
The following are my entities, controller and query handler
// Entities
public class Course : BaseAuditableEntity
{
public string? Name { get; set; }
public Guid StudentId { get; set; }
public virtual Student? Student { get; set; }
}
public class Student : BaseAuditableEntity
{
public string? Name { get; set; }
public virtual ICollection<Course> Courses{ get; private set; } = new List<Course>();
}
// Mediator query and handler
public record GetRawCoursesQuery : IRequest<IQueryable<Course>>;
public class GetCoursesQueryHandler : IRequestHandler<GetRawCoursesQuery, IQueryable<Course>>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;
public GetCoursesQueryHandler(IApplicationDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public Task<IQueryable<Course>> Handle(GetRawCoursesQuery request, CancellationToken cancellationToken)
{
return Task.FromResult(_context.Courses.AsQueryable());
}
}
// Contoller action
[HttpGet("odata")]
[EnableQuery]
public async Task<IActionResult> GetCoursesAsync(
[FromQuery] int top,
[FromQuery] int skip,
[FromQuery] string orderby,
[FromQuery] string filter,
[FromQuery] string expand)
{
var query = await _sender.Send(new GetRawCoursesQuery());
return Ok(query);
}
// GetEdmModel in Program.cs
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Course>("Courses");
var course = builder.EntityType<Course>();
course.ContainsOptional(t => t.Student);
builder.EntitySet<Student>("Students");
return builder.GetEdmModel();
}
So the above works perfectly, I can filter I can select and most importantly I can expand. All works as expected.
Also, in case anyone wondering why I have FromQuery in my action, they are needed as I use NSwag to generate my APIClient and that’s the only way for NSwag to see an OData endpoint!
Problem:
The above works fine, however it works fine because I’m using ‘Course’ and ‘Student’ entities, but I need this to be using DTOs, however when I do that everything works except Expand
Here are the changes I made
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<CourseDto>("Courses");
var course = builder.EntityType<CourseDto>();
course.ContainsOptional(t => t.Student);
builder.EntitySet<StudentDto>("Students");
return builder.GetEdmModel();
}
public record GetRawCoursesQuery : IRequest<IQueryable<CourseDto>>;
public class GetCoursesQueryHandler : IRequestHandler<GetRawCoursesQuery, IQueryable<CourseDto>>
{
private readonly IApplicationDbContext _context;
private readonly IMapper _mapper;
public GetCoursesQueryHandler(IApplicationDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public Task<IQueryable<CourseDto>> Handle(GetRawCoursesQuery request, CancellationToken cancellationToken)
{
return Task.FromResult(_mapper.ProjectTo<CourseDto>(_context.Courses));
}
}
public class CourseDto
{
public string? Name { get; set; }
public Guid StudentId { get; set; }
public virtual StudentDto? Student { get; set; }
private class Mapping : Profile
{
public Mapping()
{
CreateMap<Course, CourseDto>()
.ForMember(dest => dest.Student, opt => opt.ExplicitExpansion());
}
}
}
public class StudentDto
{
public string? Name { get; set; }
private class Mapping : Profile
{
public Mapping()
{
CreateMap<Student, StudentDto>();
}
}
}
And no changes to the Controller Action as it doesnt reference anything about Entity or Dto!
Everything works fine except the Expand, when I call a URL like
https://localhost:5001/odata/Courses?expand=Student
Which is expected to include Student in the return result within Course object, but the Student comes back null!
It seems that the issue might be related to ProjectTo, I tested this by including the property manually into ProjectTo and it worked, so I think somehow OData need to tell ProjectTo to include Expanded properties!
Appreciate it if anyone can help me understand where the issue is here!