[英]EF Core - Return mapped Many-to-Many relationship from OData using Automapper
The app type is a Hosted Blazor web-assembly.应用类型是 Hosted Blazor web-assembly。 And below are the versions of the nuget packages I am using.
以下是我正在使用的 nuget 软件包的版本。 There is an error that occurs when trying to expand a navigation property that is a many-to-many relationship.
尝试扩展多对多关系的导航属性时发生错误。 The classes are mapped to DTO classes that flattens the middle relationship class.
这些类映射到 DTO 类,使中间关系变平 class。
To run this repo, you will need the free version of SQL Server or better要运行此 repo,您将需要 SQL Server 或更好的免费版本
Set the EfCoreAutomapperOdata.Server project as the startup project and navigate to the Courses page (https://localhost:5001/courses) and click on either course.将 EfCoreAutomapperOdata.Server 项目设置为启动项目并导航到课程页面 (https://localhost:5001/courses) 并单击任一课程。 This will throw the following error:
这将引发以下错误:
System.InvalidOperationException: No generic method 'Include' on type 'Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic. at System.Linq.Expressions.Expression.FindMethod(Type type, String methodName, Type[] typeArgs, Expression[] args, BindingFlags flags)...
See here - Entity Models and here - Dto Models for class definition请参阅此处 - 实体模型和此处 - class 定义的 Dto 模型
public class AutomapperConfig: Profile { public AutomapperConfig() { CreateMap<Instructor, InstructorDto>(); CreateMap<InstructorDto, Instructor>(); CreateMap<Course, CourseDto>().ForMember(dto => dto.Students, opt => { opt.MapFrom(_ => _.Students.Select(y => y.Student)); }); CreateMap<CourseDto, Course>().ForMember(ent => ent.Students, ex => ex.MapFrom(x => x.Students.Select(y => new CourseStudent { CourseId = x.Id, StudentId = y.Id }))); CreateMap<Student, StudentDto>().ForMember(dto => dto.Courses, opt => { opt.MapFrom(x => x.Courses.Select(y => y.Course)); }).ForMember(dto => dto.Friends, opt => { opt.MapFrom(x => x.Friends.Select(y => y.Friend)); }); CreateMap<StudentDto, Student>().ForMember(ent => ent.Courses, ex => ex.MapFrom(x => x.Courses.Select(y => new CourseStudent { StudentId = x.Id, CourseId = y.Id }))); } }
Startup
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { // ------ Some code removed for brevity ------ services.AddOData(); services.AddAutoMapper(cfg => { cfg.AddExpressionMapping(); },typeof(AutomapperConfig)); // ------ Some code removed for brevity ------ } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ------ Some code removed for brevity ------ app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); endpoints.MapControllers(); endpoints.EnableDependencyInjection(); endpoints.Select().Filter().OrderBy().Count().Expand().MaxTop(1000); endpoints.MapODataRoute("odata", "odata", GetEdmModel()); endpoints.MapFallbackToFile("index.html"); }); } private IEdmModel GetEdmModel() { var builder = new ODataConventionModelBuilder(); builder.EntitySet<CourseDto>("Courses"); builder.EntitySet<InstructorDto>("Instructors"); builder.EntitySet<StudentDto>("Students"); return builder.GetEdmModel(); } }
Course Controller
public class CourseController: ODataController { protected readonly BlazorContext _context; protected readonly IMapper _mapper; public CourseController(BlazorContext context, IMapper mapper) { _context = context; _mapper = mapper; } [HttpGet] [ODataRoute("Courses")] public async Task<IActionResult> Get(ODataQueryOptions<CourseDto> options) { return Ok(await _context.Course.GetAsync(_mapper, options)); } [HttpGet] [ODataRoute("Courses({id})")] public async Task<IActionResult> Get([FromODataUri] int id, ODataQueryOptions<CourseDto> options) { return Ok((await _context.Course.GetAsync(_mapper, options)).Where(s => s.Id == id).ToList()); } }
Sample odata api query that fails
/odata/Courses?$expand=Students
I have built demo Blazor WASM app for this issue to reproduce我已经为这个问题构建了演示 Blazor WASM 应用程序来重现
To make expansions work, you need to allow for the $expand
query option to be used.要使扩展工作,您需要允许使用
$expand
查询选项。 Configure it explicitly like so:像这样显式配置它:
private IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<EstimateDto>(nameof(MyContext.Estimates))
.EntityType
.Expand(); // <-- allow expansion
builder.EntitySet<TypeDto>(nameof(MyContext.Types))
.EntityType
.Expand(); // <-- allow expansion
builder.EntitySet<SubTypeDto>(nameof(MyContext.SubTypes));
return builder.GetEdmModel();
}
You will also need to update your AutoMapper map, to allow the queries to be successfully mapped to a DTOs:您还需要更新您的 AutoMapper map,以允许查询成功映射到 DTO:
public class AutoMapperConfig : Profile
{
public AutoMapperConfig()
{
CreateMap<Estimate, EstimateDto>()
.ForMember(
dto => dto.Types,
opt => opt.MapFrom(x => x.EstimateTypes.Select(y => y.Type)));
// The following mapping is needed for expansion to work:
CreateMap<EstimateTypeRel, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(x => x.Type));
CreateMap<Type, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(x => x.SubTypes.Select(y => y.SubType)));
CreateMap<SubTypeRel, SubTypeDto>();
}
}
With that configuration being setup, there are at least two possible solutions to this issue, depending on your requirements:设置该配置后,根据您的要求,至少有两种可能的解决方案可以解决此问题:
Types
Types
If you only want to expand Types
, you will need to change your AutoMapper mappings by adding a .Where(z => z != null)
clause, because as the exception tells you, null
values are not allowed in collections, but OData includes them for the non-expanded SubType
entities:如果您只想扩展
Types
,则需要通过添加.Where(z => z != null)
子句来更改 AutoMapper 映射,因为异常告诉您, null
中不允许使用 null 值,但 OData 包括它们用于非扩展的SubType
实体:
public class AutoMapperConfig : Profile
{
public AutoMapperConfig()
{
CreateMap<Estimate, EstimateDto>()
.ForMember(
dto => dto.Types,
opt => opt.MapFrom(
x => x.EstimateTypes.Select(y => y.Type)
.Where(z => z != null))); // <-- filter out null values
CreateMap<EstimateTypeRel, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(x => x.Type));
CreateMap<Type, TypeDto>()
.ForMember(
dto => dto.SubTypes,
opt => opt.MapFrom(
x => x.SubTypes.Select(y => y.SubType)
.Where(z => z != null))); // <-- filter out null values
CreateMap<SubTypeRel, SubTypeDto>();
}
}
Then you can use the following query:然后您可以使用以下查询:
https://localhost:5001/odata/Estimates(1)?$expand=Types
SubTypes
SubTypes
The alternative is to expand the SubTypes
property as well, so the collection can be properly filled.另一种方法是也扩展
SubTypes
属性,以便可以正确填充集合。 To expand DTO mapped properties over multiple levels, use the $expand
query option in your query string like so:要在多个级别上扩展 DTO 映射属性,请在查询字符串中使用
$expand
查询选项,如下所示:
https://localhost:5001/odata/Estimates(1)?$expand=Types($expand=SubTypes)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.