OData / WebApi

OData Web API: A server library built upon ODataLib and WebApi
https://docs.microsoft.com/odata
Other
856 stars 475 forks source link

Errors using DTOs with polymorphism and Entity Framework #2175

Open joeburdick opened 4 years ago

joeburdick commented 4 years ago

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?

jacekkulis commented 3 years ago

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

lenardchristopher commented 10 months ago

Bumping too! Thanks