简体   繁体   English

使用实体框架和 OData 查询的导航 DTO 属性

[英]Navigational DTO properties using Entity Framework with OData Queries

Development environment开发环境

  • ASP.NET Core 3.1 ASP.NET 核心 3.1
  • Microsoft.EntityFrameworkCore 3.1.9 Microsoft.EntityFrameworkCore 3.1.9
  • Microsoft.AspNetCore.OData 7.5.1 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 EF 核心上下文

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 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();
}

ASP.NET Core Controller 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()
        });
    }
}

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?我似乎能够将顶级类作为 dto 返回,只公开客户端可能需要的属性,但是否也可以公开并返回 dto 作为导航属性?

Non-dto output非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
        }
      ]
    }
  ]
}

Desired output (with the $filter and $expand query above)所需的输出(使用上面的 $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
        }
      ]
    }
  ]
}

Update #1更新 #1

If I add Automapper into the mix and try using the ProjectTo method with the following code:如果我将 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);
    }

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.

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?我似乎能够将顶级类作为 dto 返回,只公开客户端可能需要的属性,但是否也可以公开并返回 dto 作为导航属性?

It's possible, but you need to solve some modelling and implementation specific problems.这是可能的,但您需要解决一些特定于建模和实现的问题。

First, the modelling.第一,建模。 OData only supports collection navigation properties to entity types . OData 仅支持实体类型的集合导航属性。 So in order to map ComputerDto.Disks property as navigation property, you need to make DiskDto entity type.因此,为了将ComputerDto.Disks属性映射为导航属性,您需要创建DiskDto实体类型。 Which in turn requires it to have a key.这反过来又要求它有一把钥匙。 So either add Id property to it, or associate some other property (for instance, Letter ) to it:因此,要么向其添加Id属性,要么将其他一些属性(例如, Letter )与其关联:

//builder.ComplexType<DiskDto>();
builder.EntityType<DiskDto>().HasKey(e => e.Letter);

Now the Disks property won't be included w/o $expand option and also will eliminate the original OData exception.现在Disks属性将不会包含在没有$expand选项的情况下,并且还将消除原始 OData 异常。

This was all about OData Edm model and enabling $expand options for Disks .这完全是关于 OData Edm 模型和为Disks启用$expand选项。

The next problem to be solved is related to OData and EF Core query implementation details.下一个要解决的问题与 OData 和 EF Core 查询实现细节有关。 Running the filtered query (w/o $expand ) produces the desired JSON output (no Disks included), but the generated EF Core SQL query is运行过滤查询(不带$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]

As you can see, it includes unnecessary joins and columns, which is inefficient.如您所见,它包括不必要的连接和列,这是低效的。

And with $expand options you get the VisitLambda exception, which is from EF Core 3.1 query translation pipeline and is caused by the ToList() call in the Disks member projection, which in turn is needed because the target property type is ICollection<DiskDto> and w/o it you get compile time error.使用$expand选项,您会得到VisitLambda异常,该异常来自 EF Core 3.1 查询转换管道,由Disks成员投影中的ToList()调用引起,而这又是必需的,因为目标属性类型是ICollection<DiskDto>没有它,你会得到编译时错误。 It can be solved by making the property type IEnumerable<DiskDto> and removing the ToList() from projection, which would eliminate the exception, but again will produce the even more inefficient SQL query可以通过使属性类型为IEnumerable<DiskDto>并从投影中删除ToList()来解决,这将消除异常,但再次会产生效率更低的 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]

All that means is that trying to use OData query directly over EF Core projection query is problematic.所有这一切都意味着尝试直接通过 EF Core 投影查询使用 OData 查询是有问题的。

So as a solution for the implementation problems I would suggest AutoMapper.Extensions.OData extension which:因此,作为实现问题的解决方案,我建议AutoMapper.Extensions.OData扩展:

Creates LINQ expressions from ODataQueryOptions and executes the query.ODataQueryOptions创建 LINQ 表达式并执行查询。

What you need is to install the package AutoMapper.AspNetCore.OData.EFCore , use AutoMapper configuration similar to this (the essential is to enable null collections and explicit expansion)您需要的是安装包AutoMapper.AspNetCore.OData.EFCore ,使用类似于此的 AutoMapper 配置(关键是启用空集合和显式扩展)

cfg.AllowNullCollections = true;
cfg.CreateMap<Computer, ComputerDto>()
    .ForAllMembers(opt => opt.ExplicitExpansion());
cfg.CreateMap<Disk, DiskDto>()
    .ForAllMembers(opt => opt.ExplicitExpansion());

(note: with this approach the property type can stay ICollection<DiskDto> ) (注意:使用这种方法,属性类型可以保持ICollection<DiskDto>

and change the controller method similar to this (the essential is to not use EnableQuery , add options argument and return IEnumerable / ICollection instead of IQueryable )并更改与此类似的控制器方法(关键是不要使用EnableQuery ,添加 options 参数并返回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);

Now both outputs will be as expected, as well as the generated SQL queries:现在两个输出都将如预期的那样,以及生成的 SQL 查询:

  • (no expand) (不展开)

output:输出:

{
    "@odata.context": "https://localhost:5001/api/$metadata#Collection(ODataTest.Dtos.ComputerDto)",
    "value": [
        {
            "Id": 1,
            "Name": "TestComputer1"
        },
        {
            "Id": 2,
            "Name": "TestComputer2"
        }
    ]
}

SQL query: SQL查询:

SELECT [c].[Id], [c].[Name]
FROM [Computers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N't%')
  • with $expand=disks使用$expand=disks

output:输出:

{
    "@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
                }
            ]
        }
    ]
}

SQL query: 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]

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM