4
votes
Info

The app type is a Hosted Blazor web-assembly. And below are the versions of the nuget packages I am using. 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.

  • .Net core Version="3.1"
  • AutoMapper Version="10.0.0"
  • AutoMapper.AspNetCore.OData.EFCore Version="2.0.1"
  • AutoMapper.Extensions.ExpressionMapping Version="4.0.1"
  • AutoMapper.Extensions.Microsoft.DependencyInjection Version="8.0.1"
  • Microsoft.AspNetCore.Components.WebAssembly.Server Version="3.2.1"
  • Microsoft.AspNetCore.OData Version="7.5.0"

To run this repo, you will need the free version of SQL Server or better

Set the EfCoreAutomapperOdata.Server project as the startup project and navigate to the Courses page (https://localhost:5001/courses) and click on either course. 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)...

Models

See here - Entity Models and here - Dto Models for class definition

Automapper Config
    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

Repro

I have built demo Blazor WASM app for this issue to reproduce

Repository

1
Where is the exception being thrown? (Or easier, just add the full exception including stack trace to the post.)lauxjpn
I added the stack trace and pointed out in the controller exactly where the error is thrownA. Dixon

1 Answers

2
votes

General Setup

To make expansions work, you need to allow for the $expand query option to be used. 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:

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:

A) Only expand 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:

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

B) Also expand SubTypes

The alternative is to expand the SubTypes property as well, so the collection can be properly filled. To expand DTO mapped properties over multiple levels, use the $expand query option in your query string like so:

https://localhost:5001/odata/Estimates(1)?$expand=Types($expand=SubTypes)