WebApi icon indicating copy to clipboard operation
WebApi copied to clipboard

Errors using DTOs with polymorphism and Entity Framework

Open joeburdick opened this issue 4 years ago • 2 comments

When projecting database models to DTOs with inheritance before applying OData queries, expressions are generated to determine type that are not compatible with Entity Framework 6.

Assemblies affected

Microsoft.AspnetCore.OData (7.4.0)

Reproduce steps

Database models and API DTOs are defined as follows:

public class Animal
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }
}

public class Cat : Animal { }

public class Dog : Animal { }

public class CatDTO : AnimalDTO { }

public class DogDTO : AnimalDTO { }

public class AnimalDTO
{
    [Key]
    public int Id { get; set; }

    public string Name { get; set; }
}

Startup is configured like so:

public void ConfigureServices(IServiceCollection services)
{
        services.AddControllers()
                    .AddNewtonsoftJson();
        services.AddOData();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
        if (env.IsDevelopment())
        {
                app.UseDeveloperExceptionPage();
        }

        app.UseHttpsRedirection();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
                {
                        endpoints.MapControllers();
                        endpoints.MapODataRoute("odata", "odata", GetEdmModel());
                        endpoints.Select()
                                        .Filter()
                                        .OrderBy()
                                        .Count()
                                        .MaxTop(10);
                });
}

IEdmModel GetEdmModel()
{
        var odataBuilder = new ODataConventionModelBuilder();
        odataBuilder.EntitySet<AnimalDTO>("Animal");
        odataBuilder.EntitySet<CatDTO>("Cat")
                .EntityType.DerivesFrom<AnimalDTO>();
        odataBuilder.EntitySet<DogDTO>("Dog")
                .EntityType.DerivesFrom<AnimalDTO>();

        return odataBuilder.GetEdmModel();
}

Controller is defined like:

public class AnimalController : ODataController
    {

        [HttpGet]
        [EnableQuery]
        public IQueryable<AnimalDTO> Get()
        {
            var context = new MyContext();
            var animals= context.Animals.Select(a => new AnimalDTO
                                                    {
                                                        Id = a.Id,
                                                        Name = a.Name
                                                    });
            return animals;
        }
    }

A request is made to endpoint:

/odata/animal?$select=name

Expected result

Response should have body like:

{
  "@odata.context": "https://localhost:5001/odata/$metadata#Animal(Name)",
  "value": [
    {
      "@odata.type": "#WebApplication.Cat",
      "Name": "Whiskers"
    },
    {
      "@odata.type": "#WebApplication.Dog",
      "Name": "Spot"
    }
  ]
}

Actual result

The following exception is thrown:

System.NotSupportedException: The 'TypeIs' expression with an input of type 'WebApplication.AnimalDTO' and a check of type 'WebApplication.DogDTO' is not supported. Only entity types and complex types are supported in LINQ to Entities queries.

Additional detail

I understand that the underlying cause is the OData package generating TypeIs expressions based on the inheritance structure as defined in the Edm, which would work fine if the entities were the Enitity Framework database entities and not DTOs.

I also understand that EF limitations prevent the items from being materialized as separate more derived DTO types and that there seems to be no way to get EF to include type information without using TypeIs expressions.

What I am wondering is whether it would be possible to somehow override Odata's type determination expressions to allow me to use my own mapped discriminator field or some other logic to determine the more derived types so that this error is not thrown and the "@odata.type" field can be properly included.

For example in this case a discriminator field could be computed like this during the projection:

context.Animals.Select(a => new AnimalDTO
                                                     {
                                                         Id = a.Id,
                                                         Name = a.Name,
                                                         Discriminator = a as Cat != null ? "Cat" : a as Dog != null ? "Dog" : null
                                                     });

Could it be possible to configure the OData library to use this Discriminator field to compute "@odata.type" instead of attempting to use the TypeIs expressions or reflection after materialization? How feasible would it be to allow for this configuration?

joeburdick avatar May 26 '20 21:05 joeburdick

Bump. Maybe there is already done some work in this area?

jacekkulis avatar Oct 09 '21 11:10 jacekkulis

Bumping too! Thanks

lenardchristopher avatar Nov 24 '23 19:11 lenardchristopher