Entity Framework inheritance and InterfaceType - Query returns null for base types
Closed this issue · 2 comments
Is there an existing issue for this?
- I have searched the existing issues
Product
Hot Chocolate
Describe the bug
When using EF inheritance and an interface type in HC, and the query returns an entity of the base type, then HC returns null.
(See reproduction steps for a complete example)
Running this query:
query {
foos(where: {id: {eq: 1}}) { #The entity with id = 1 is of type Foo
id
x
}
}
returns this error to the client:
{
"errors": [
{
"message": "Cannot return null for non-nullable field.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"foos"
],
"extensions": {
"code": "HC0018"
}
}
]
}
Running this query:
query {
foos(where: {id: {eq: 2}}) { #The entity with id = 2 is of type Bar
id
x
}
}
Succeeds with:
{
"data": {
"foos": [
{
"id": 2,
"x": 2
}
]
}
}
Looking further into debug logs we can see that the compiled query expression for EF becomes:
DbSet<Foo>()
.Where(_s0 => _s0.Id == __p_0)
.Select(_s1 => (_s1 is Bar) ? (Foo)new Bar{
Id = ((Bar)_s1).Id,
X = ((Bar)_s1).X
}
: null)
I.e. it seems to never test for the InterfaceType type? Am I configuring something wrong or is this a bug?
Steps to reproduce
- Create a new asp.net core project on .NET 7 using minimal api.
- Add this to the .csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HotChocolate.AspNetCore" Version="12.16.0" />
<PackageReference Include="HotChocolate.Data.EntityFramework" Version="12.16.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.2" />
</ItemGroup>
</Project>
- Add this to Program.cs:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddDbContext<GraphQLDbContext>(o => o
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=GraphQLDbContext;Trusted_Connection=True;"));
builder.Services
.AddGraphQLServer()
.RegisterDbContext<GraphQLDbContext>()
.AddQueryType<Query>()
.AddType<Bar>()
.AddProjections()
.AddFiltering();
var app = builder.Build();
var serviceScopeFactory = app.Services.GetRequiredService<IServiceScopeFactory>();
using (var scope = serviceScopeFactory.CreateScope())
using (var ctx = scope.ServiceProvider.GetRequiredService<GraphQLDbContext>())
{
ctx.Database.EnsureDeleted();
ctx.Database.EnsureCreated();
ctx.Foos.Add(new Foo {Id = 1, X = 1});
ctx.Foos.Add(new Bar {Id = 2, X = 2, Y = 3});
ctx.SaveChanges();
}
app.MapGet("/", () => "Hello World!");
app.MapGraphQL();
app.Run();
public class GraphQLDbContext : DbContext
{
protected GraphQLDbContext()
{
}
public GraphQLDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Foo> Foos => Set<Foo>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Foo>().ToTable("Foo");
modelBuilder.Entity<Bar>().ToTable("Bar");
}
}
public class Query
{
[UseProjection]
[UseFiltering]
public IEnumerable<Foo> GetFoos(GraphQLDbContext dbContext) => dbContext.Foos;
}
[InterfaceType("Foo")]
public class Foo
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public int X { get; set; }
}
public class Bar : Foo
{
public int Y { get; set; }
}
- Run a query that returns all entities or filter the entity with id = 1 (which is a Foo, i.e. the InterfaceType)
Relevant log output
dbug: Microsoft.EntityFrameworkCore.Query[10111]
Compiling query expression:
'DbSet<Foo>()
.Where(_s0 => _s0.Id == __p_0)
.Select(_s1 => (_s1 is Bar) ? (Foo)new Bar{
Id = ((Bar)_s1).Id,
X = ((Bar)_s1).X
}
: null)'
dbug: Microsoft.EntityFrameworkCore.Query[10107]
Generated query execution expression:
'queryContext => new SingleQueryingEnumerable<Foo>(
(RelationalQueryContext)queryContext,
RelationalCommandCache.QueryExpression(
Client Projections:
0 -> 0
1 -> 1
2 -> 2
SELECT NotEqual(b.Id), f.Id, f.X
FROM Foo AS f
LEFT JOIN Bar AS b ON f.Id == b.Id
WHERE f.Id == @__p_0),
null,
Func<QueryContext, DbDataReader, ResultContext, SingleQueryResultCoordinator, Foo>,
GraphQLDbContext,
False,
False,
True
)'
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (3ms) [Parameters=[@__p_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
SELECT CASE
WHEN [b].[Id] IS NOT NULL THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END, [f].[Id], [f].[X]
FROM [Foo] AS [f]
LEFT JOIN [Bar] AS [b] ON [f].[Id] = [b].[Id]
WHERE [f].[Id] = @__p_0
Additional Context?
No response
Version
12.16.0
Here's a full repro of the issue: andagr/hc-issue-5694
I think the problem here is that you do not have an ObjectType<Foo>
registered.
In GraphQL you cannot return an "interface". You always return implementations. If in your model you can instantiate "Foo" and return it, then you have to register a ObjectType
of Foo aswell otherwise you would get an runtime error if you return Foo.
If you register a ObjectType<Foo>
, then Foo
would be in the list of possible types and would be resolved here correctly:
https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Data/src/Data/Projections/SelectionVisitor%601.cs#L114-L115