Development environment
- ASP.NET Core 3.1
- Microsoft.EntityFrameworkCore 3.1.9
- Microsoft.AspNetCore.OData 7.5.1
Models
public class Computer
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Disk> Disks { get; set; }
}
public class Disk
{
public int Id { get; set; }
public string Letter { get; set; }
public float Capacity { get; set; }
public int? ComputerId { get; set; }
public virtual Computer Computer { get; set; }
}
Dtos
public class ComputerDto
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<DiskDto> Disks { get; set; }
}
public class DiskDto
{
public string Letter { get; set; }
public float Capacity { get; set; }
}
EF Core Context
public class ComputerContext : DbContext
{
public DbSet<Computer> Computers { get; set; }
public DbSet<Disk> Disks { get; set;}
public ComputerContext(DbContextOptions<ComputerContext> options)
: base(options)
{
}
}
OData EDM Model
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Computer>("Computers");
builder.EntitySet<Disk>("Disks");
builder.ComplexType<ComputerDto>();
builder.ComplexType<DiskDto>();
return builder.GetEdmModel();
}
ASP.NET Core Controller
[Route("api/[controller]")]
[ApiController]
public class ComputersController : ControllerBase
{
private readonly ComputerContext context;
public ComputersController(ComputerContext context)
{
this.context = context;
}
[HttpGet]
[EnableQuery]
public IQueryable<ComputerDto> GetComputers()
{
return this.context.Computers.Select(c => new ComputerDto
{
Id = c.Id,
Name = c.Name,
Disks = c.Disks.Select(d => new DiskDto
{
Letter = d.Letter,
Capacity = d.Capacity
}).ToList()
});
}
}
This query works but Disks is already expanded because I'm manually creating the list.
https://localhost:46324/api/computers?$filter=startswith(name,'t')
and output
{
"@odata.context": "https://localhost:46324/api/$metadata#Collection(ODataPlayground.Dtos.ComputerDto)",
"value": [
{
"Id": 14,
"Name": "TestComputer1",
"Disks": [
{
"Letter": "C",
"Capacity": 234.40
},
{
"Letter": "D",
"Capacity": 1845.30
}
]
},
{
"Id": 15,
"Name": "TestComputer2",
"Disks": [
{
"Letter": "C",
"Capacity": 75.50
},
{
"Letter": "D",
"Capacity": 499.87
}
]
}
]
}
If I then try to expand "Disks" with the following query I get an error:
https://localhost:46324/api/computers?$filter=startswith(name,'t')&$expand=disks
error
{
"error": {
"code": "",
"message": "The query specified in the URI is not valid. Property 'disks' on type 'ODataPlayground.Dtos.ComputerDto' is not a navigation property or complex property. Only navigation properties can be expanded.",
"details": [],
"innererror": {
"message": "Property 'disks' on type 'ODataPlayground.Dtos.ComputerDto' is not a navigation property or complex property. Only navigation properties can be expanded.",
"type": "Microsoft.OData.ODataException",
"stacktrace": "...really long stack trace removed for compactness..."
}
}
}
Question
- I seem to be able to return the top level class as a dto, only exposing the properties a client might need but is it also possible to expose and return a dto as a navigation property?
Non-dto output
{
"@odata.context": "https://localhost:46324/api/$metadata#Collection(ODataPlayground.Dtos.ComputerDto)",
"value": [
{
"Id": 14,
"Name": "TestComputer1",
"Disks": [
{
"Id": 16,
"ComputerId": 14,
"Letter": "C",
"Capacity": 234.40
},
{
"Id": 17,
"ComputerId": 14,
"Letter": "D",
"Capacity": 1845.30
}
]
}
]
}
Desired output (with the $filter and $expand query above)
{
"@odata.context": "https://localhost:46324/api/$metadata#Collection(ODataPlayground.Dtos.ComputerDto)",
"value": [
{
"Id": 14,
"Name": "TestComputer1",
"Disks": [
{
"Letter": "C",
"Capacity": 234.40
},
{
"Letter": "D",
"Capacity": 1845.30
}
]
}
]
}
Update #1
If I add Automapper into the mix and try using the ProjectTo
method with the following code:
//// Inject context and mapper
public ComputersController(ComputerContext context, IMapper mapper)
{
this.context = context;
this.mapper = mapper;
}
[HttpGet]
[EnableQuery]
public IQueryable<ComputerDto> GetComputers()
{
return this.context.Computers.ProjectTo<ComputerDto>(mapper.ConfigurationProvider);
}
I get a different error:
InvalidOperationException: When called from 'VisitLambda', rewriting a node of type
'System.Linq.Expressions.ParameterExpression' must return a non - null value of the same type.
Alternatively, override 'VisitLambda' and change it to not visit children of this type.
disks != Disks
, it can be the error ? – lupaulus$expand=Disks
does not produce a different result. – Bluecakes