Blu*_*kes 4 c# odata entity-framework-core asp.net-core
开发环境
楷模
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; }
}
Run Code Online (Sandbox Code Playgroud)
托斯
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; }
}
Run Code Online (Sandbox Code Playgroud)
EF 核心上下文
public class ComputerContext : DbContext
{
public DbSet<Computer> Computers { get; set; }
public DbSet<Disk> Disks { get; set;}
public ComputerContext(DbContextOptions<ComputerContext> options)
: base(options)
{
}
}
Run Code Online (Sandbox Code Playgroud)
OData EDM 模型
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();
}
Run Code Online (Sandbox Code Playgroud)
ASP.NET 核心控制器
[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()
});
}
}
Run Code Online (Sandbox Code Playgroud)
此查询有效,但磁盘已扩展,因为我正在手动创建列表。
https://localhost:46324/api/computers?$filter=startswith(name,'t')
Run Code Online (Sandbox Code Playgroud)
和输出
{
"@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
}
]
}
]
}
Run Code Online (Sandbox Code Playgroud)
如果我然后尝试使用以下查询扩展“磁盘”,则会收到错误消息:
https://localhost:46324/api/computers?$filter=startswith(name,'t')&$expand=disks
Run Code Online (Sandbox Code Playgroud)
错误
{
"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..."
}
}
}
Run Code Online (Sandbox Code Playgroud)
题
非dto输出
{
"@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
}
]
}
]
}
Run Code Online (Sandbox Code Playgroud)
所需的输出(使用上面的 $filter 和 $expand 查询)
{
"@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
}
]
}
]
}
Run Code Online (Sandbox Code Playgroud)
更新 #1
如果我将 Automapper 添加到组合中并尝试使用ProjectTo以下代码的方法:
//// 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);
}
Run Code Online (Sandbox Code Playgroud)
我得到一个不同的错误:
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.
Run Code Online (Sandbox Code Playgroud)
我似乎能够将顶级类作为 dto 返回,只公开客户端可能需要的属性,但是否也可以公开并返回 dto 作为导航属性?
这是可能的,但您需要解决一些特定于建模和实现的问题。
第一,建模。OData 仅支持实体类型的集合导航属性。因此,为了将ComputerDto.Disks属性映射为导航属性,您需要创建DiskDto实体类型。这反过来又要求它有一把钥匙。因此,要么Id为其添加属性,要么将其他一些属性(例如,Letter)与其关联:
//builder.ComplexType<DiskDto>();
builder.EntityType<DiskDto>().HasKey(e => e.Letter);
Run Code Online (Sandbox Code Playgroud)
现在该Disks属性将不包含在没有$expand选项的情况下,并且还将消除原始 OData 异常。
这是所有关于OData的EDM模型,并实现$expand了选项Disks。
下一个要解决的问题与 OData 和 EF Core 查询实现细节有关。运行过滤查询 (w/o $expand) 会产生所需的 JSON 输出(不Disks包括在内),但生成的 EF Core SQL 查询是
SELECT [c].[Id], [c].[Name], [d].[Letter], [d].[Capacity], [d].[Id]
FROM [Computers] AS [c]
LEFT JOIN [Disks] AS [d] ON [c].[Id] = [d].[ComputerId]
WHERE (@__TypedProperty_0 = N'') OR ([c].[Name] IS NOT NULL AND (LEFT([c].[Name], LEN(@__TypedProperty_0)) = @__TypedProperty_0))
ORDER BY [c].[Id], [d].[Id]
Run Code Online (Sandbox Code Playgroud)
如您所见,它包括不必要的连接和列,这是低效的。
使用$expand选项,您会得到VisitLambda异常,该异常来自 EF Core 3.1 查询转换管道,由成员投影中的ToList()调用引起Disks,而这又是必需的,因为目标属性类型是ICollection<DiskDto>并且没有它,您会得到编译时错误。可以通过创建属性类型IEnumerable<DiskDto>并删除ToList()from 投影来解决,这将消除异常,但再次会产生更低效的 SQL 查询
SELECT [c].[Id], [c].[Name], [d].[Letter], [d].[Capacity], [d].[Id], @__TypedProperty_2, [d0].[Letter], [d0].[Capacity], CAST(1 AS bit), [d0].[Id]
FROM [Computers] AS [c]
LEFT JOIN [Disks] AS [d] ON [c].[Id] = [d].[ComputerId]
LEFT JOIN [Disks] AS [d0] ON [c].[Id] = [d0].[ComputerId]
WHERE (@__TypedProperty_0 = N'') OR ([c].[Name] IS NOT NULL AND (LEFT([c].[Name], LEN(@__TypedProperty_0)) = @__TypedProperty_0))
ORDER BY [c].[Id], [d].[Id], [d0].[Id]
Run Code Online (Sandbox Code Playgroud)
所有这一切都意味着尝试直接通过 EF Core 投影查询使用 OData 查询是有问题的。
因此,作为实现问题的解决方案,我建议AutoMapper.Extensions.OData扩展:
ODataQueryOptions根据查询创建 LINQ 表达式并执行查询。
您需要的是安装包AutoMapper.AspNetCore.OData.EFCore,使用类似于此的 AutoMapper 配置(关键是启用空集合和显式扩展)
cfg.AllowNullCollections = true;
cfg.CreateMap<Computer, ComputerDto>()
.ForAllMembers(opt => opt.ExplicitExpansion());
cfg.CreateMap<Disk, DiskDto>()
.ForAllMembers(opt => opt.ExplicitExpansion());
Run Code Online (Sandbox Code Playgroud)
(注意:使用这种方法,属性类型可以保留ICollection<DiskDto>)
并更改类似于此的控制器方法(关键是不要使用EnableQuery,添加选项参数并返回IEnumerable/ICollection而不是IQueryable)
using AutoMapper.AspNet.OData;
[HttpGet]
public async Task<IEnumerable<ComputerDto>> GetComputers(
ODataQueryOptions<ComputerDto> options) =>
await context.Computers.GetAsync(mapper, options, HandleNullPropagationOption.False);
Run Code Online (Sandbox Code Playgroud)
现在两个输出都将如预期的那样,以及生成的 SQL 查询:
输出:
{
"@odata.context": "https://localhost:5001/api/$metadata#Collection(ODataTest.Dtos.ComputerDto)",
"value": [
{
"Id": 1,
"Name": "TestComputer1"
},
{
"Id": 2,
"Name": "TestComputer2"
}
]
}
Run Code Online (Sandbox Code Playgroud)
SQL查询:
SELECT [c].[Id], [c].[Name]
FROM [Computers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N't%')
Run Code Online (Sandbox Code Playgroud)
$expand=disks输出:
{
"@odata.context": "https://localhost:5001/api/$metadata#Collection(ODataTest.Dtos.ComputerDto)",
"value": [
{
"Id": 1,
"Name": "TestComputer1",
"Disks": [
{
"Letter": "C",
"Capacity": 234.4
},
{
"Letter": "D",
"Capacity": 1845.3
}
]
},
{
"Id": 2,
"Name": "TestComputer2",
"Disks": [
{
"Letter": "C",
"Capacity": 75.5
},
{
"Letter": "D",
"Capacity": 499.87
}
]
}
]
}
Run Code Online (Sandbox Code Playgroud)
SQL查询:
SELECT [c].[Id], [c].[Name], [d].[Id], [d].[Capacity], [d].[ComputerId], [d].[Letter]
FROM [Computers] AS [c]
LEFT JOIN [Disks] AS [d] ON [c].[Id] = [d].[ComputerId]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N't%')
ORDER BY [c].[Id], [d].[Id]
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
892 次 |
| 最近记录: |