Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CS1591;NU5104;CS1573;CS9107;NU1608;NU1109</NoWarn>
<Version>34.2.0-beta.6</Version>
<Version>34.2.0-beta.7</Version>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
<PackageTags>EntityFrameworkCore, EntityFramework, GraphQL</PackageTags>
Expand Down
9 changes: 9 additions & 0 deletions src/GraphQL.EntityFramework/ForeignKeyExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ static IReadOnlySet<string> GetForeignKeys(IEntityType entity)
}
}

// Include TPH discriminator property so projected entities maintain correct type identity.
// Without this, projected entities get the default discriminator value (e.g. enum value 0)
// instead of the actual value, causing downstream code that switches on the discriminator to fail.
var discriminator = entity.FindDiscriminatorProperty();
if (discriminator != null)
{
foreignKeyNames.Add(discriminator.Name);
}

return foreignKeyNames;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
public abstract class DiscriminatorBaseEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public DiscriminatorType EntityType { get; set; }
public string? Property { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public class DiscriminatorDerivedAEntity : DiscriminatorBaseEntity
{
public string? DerivedAProperty { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public class DiscriminatorDerivedAGraphType :
EfObjectGraphType<IntegrationDbContext, DiscriminatorDerivedAEntity>
{
public DiscriminatorDerivedAGraphType(IEfGraphQLService<IntegrationDbContext> graphQlService) :
base(graphQlService) =>
AutoMap();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public class DiscriminatorDerivedBEntity : DiscriminatorBaseEntity
{
public string? DerivedBProperty { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public class DiscriminatorDerivedBGraphType :
EfObjectGraphType<IntegrationDbContext, DiscriminatorDerivedBEntity>
{
public DiscriminatorDerivedBGraphType(IEfGraphQLService<IntegrationDbContext> graphQlService) :
base(graphQlService) =>
AutoMap();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public enum DiscriminatorType
{
TypeA,
TypeB
}
16 changes: 16 additions & 0 deletions src/Tests/IntegrationTests/IntegrationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ protected override void OnConfiguring(DbContextOptionsBuilder builder) =>
public DbSet<FilterBaseEntity> FilterBaseEntities { get; set; } = null!;
public DbSet<FilterDerivedEntity> FilterDerivedEntities { get; set; } = null!;
public DbSet<FilterReferenceEntity> FilterReferenceEntities { get; set; } = null!;
public DbSet<DiscriminatorBaseEntity> DiscriminatorBaseEntities { get; set; } = null!;
public DbSet<DiscriminatorDerivedAEntity> DiscriminatorDerivedAEntities { get; set; } = null!;
public DbSet<DiscriminatorDerivedBEntity> DiscriminatorDerivedBEntities { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand Down Expand Up @@ -126,5 +129,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasBaseType<FilterBaseEntity>();
modelBuilder.Entity<FilterReferenceEntity>()
.OrderBy(_ => _.Property);

// Configure TPH inheritance with CLR discriminator property for DiscriminatorBaseEntity hierarchy
modelBuilder.Entity<DiscriminatorBaseEntity>()
.HasDiscriminator(_ => _.EntityType)
.HasValue<DiscriminatorDerivedAEntity>(DiscriminatorType.TypeA)
.HasValue<DiscriminatorDerivedBEntity>(DiscriminatorType.TypeB)
.IsComplete();
modelBuilder.Entity<DiscriminatorDerivedAEntity>()
.HasBaseType<DiscriminatorBaseEntity>()
.OrderBy(_ => _.Property);
modelBuilder.Entity<DiscriminatorDerivedBEntity>()
.HasBaseType<DiscriminatorBaseEntity>()
.OrderBy(_ => _.Property);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
target:
{
"data": {
"discriminatorDerivedAEntities": [
{
"entityType": "TYPE_A",
"property": "First"
},
{
"entityType": "TYPE_A",
"property": "Second"
}
]
}
},
sql: {
Text:
select d.Id,
d.EntityType,
d.Property
from DiscriminatorBaseEntities as d
where d.EntityType = 0
order by d.Property
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
target:
{
"data": {
"discriminatorDerivedAEntity": {
"property": "Value1"
}
}
},
sql: {
Text:
select top (2) d.Id,
d.EntityType,
d.Property
from DiscriminatorBaseEntities as d
where d.EntityType = 0
and d.Id = 'Guid_1'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
target:
{
"data": {
"discriminatorDerivedBEntity": {
"entityType": "TYPE_B",
"property": "ValueB"
}
}
},
sql: {
Text:
select top (2) d.Id,
d.EntityType,
d.Property
from DiscriminatorBaseEntities as d
where d.EntityType = 1
and d.Id = 'Guid_1'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@
filterReferenceEntities(id: ID, ids: [ID!], where: [WhereExpression!], orderBy: [OrderBy!], skip: Int, take: Int): [FilterReference!]!
filterReferenceEntity(id: ID, ids: [ID!], where: [WhereExpression!]): FilterReference!
filterReferenceEntityNullable(id: ID, ids: [ID!], where: [WhereExpression!]): FilterReference
discriminatorDerivedAEntities(id: ID, ids: [ID!], where: [WhereExpression!], orderBy: [OrderBy!], skip: Int, take: Int): [DiscriminatorDerivedA!]!
discriminatorDerivedAEntity(id: ID, ids: [ID!], where: [WhereExpression!]): DiscriminatorDerivedA!
discriminatorDerivedBEntities(id: ID, ids: [ID!], where: [WhereExpression!], orderBy: [OrderBy!], skip: Int, take: Int): [DiscriminatorDerivedB!]!
discriminatorDerivedBEntity(id: ID, ids: [ID!], where: [WhereExpression!]): DiscriminatorDerivedB!
}

type CustomType {
Expand Down Expand Up @@ -757,6 +761,25 @@ type FilterBase {
id: ID!
}

type DiscriminatorDerivedA {
derivedAProperty: String
entityType: DiscriminatorType!
id: ID!
property: String
}

enum DiscriminatorType {
TYPE_A
TYPE_B
}

type DiscriminatorDerivedB {
derivedBProperty: String
entityType: DiscriminatorType!
id: ID!
property: String
}

type Mutation {
parentEntityMutation(id: ID, ids: [ID!], where: [WhereExpression!]): Parent!
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
public partial class IntegrationTests
{
/// <summary>
/// Verifies that when querying a concrete derived type in a TPH hierarchy with a CLR
/// discriminator property, the discriminator column is included in the SQL SELECT projection
/// even when it is not explicitly requested in the GraphQL query.
/// </summary>
[Fact]
public async Task CLR_discriminator_included_in_single_field_projection()
{
var entity = new DiscriminatorDerivedAEntity
{
Property = "Value1",
DerivedAProperty = "DerivedA1"
};

var query =
$$"""
{
discriminatorDerivedAEntity(id: "{{entity.Id}}")
{
property
}
}
""";

await using var database = await sqlInstance.Build();
await RunQuery(database, query, null, null, false, [entity]);
}

/// <summary>
/// Verifies that the CLR discriminator property returns the correct enum value
/// when explicitly requested in the GraphQL query, not the default enum value.
/// Without the fix, the projected entity would have EntityType = TypeA (default 0)
/// regardless of the actual discriminator value.
/// </summary>
[Fact]
public async Task CLR_discriminator_returns_correct_value_when_queried()
{
var entityA = new DiscriminatorDerivedAEntity
{
Property = "ValueA",
DerivedAProperty = "DerivedA"
};
var entityB = new DiscriminatorDerivedBEntity
{
Property = "ValueB",
DerivedBProperty = "DerivedB"
};

var query =
$$"""
{
discriminatorDerivedBEntity(id: "{{entityB.Id}}")
{
entityType
property
}
}
""";

await using var database = await sqlInstance.Build();
await RunQuery(database, query, null, null, false, [entityA, entityB]);
}

/// <summary>
/// Verifies that the CLR discriminator column is included in projection when querying
/// a list of derived entities, ensuring all returned entities have the correct
/// discriminator value.
/// </summary>
[Fact]
public async Task CLR_discriminator_included_in_query_field_projection()
{
var entity1 = new DiscriminatorDerivedAEntity
{
Property = "First",
DerivedAProperty = "DerivedA1"
};
var entity2 = new DiscriminatorDerivedAEntity
{
Property = "Second",
DerivedAProperty = "DerivedA2"
};
// This TypeB entity should not appear in TypeA query results
var entity3 = new DiscriminatorDerivedBEntity
{
Property = "Third",
DerivedBProperty = "DerivedB1"
};

var query =
"""
{
discriminatorDerivedAEntities
{
entityType
property
}
}
""";

await using var database = await sqlInstance.Build();
await RunQuery(database, query, null, null, false, [entity1, entity2, entity3]);
}
}
16 changes: 16 additions & 0 deletions src/Tests/IntegrationTests/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,5 +365,21 @@ public Query(IEfGraphQLService<IntegrationDbContext> efGraphQlService)
name: "filterReferenceEntityNullable",
resolve: _ => _.DbContext.FilterReferenceEntities,
nullable: true);

AddQueryField(
name: "discriminatorDerivedAEntities",
resolve: _ => _.DbContext.DiscriminatorDerivedAEntities);

AddSingleField(
name: "discriminatorDerivedAEntity",
resolve: _ => _.DbContext.DiscriminatorDerivedAEntities);

AddQueryField(
name: "discriminatorDerivedBEntities",
resolve: _ => _.DbContext.DiscriminatorDerivedBEntities);

AddSingleField(
name: "discriminatorDerivedBEntity",
resolve: _ => _.DbContext.DiscriminatorDerivedBEntities);
}
}
2 changes: 2 additions & 0 deletions src/Tests/IntegrationTests/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public Schema(IServiceProvider resolver) :
RegisterTypeMapping(typeof(FilterReferenceEntity), typeof(FilterReferenceGraphType));
RegisterTypeMapping(typeof(FilterBaseEntity), typeof(FilterBaseGraphType));
RegisterTypeMapping(typeof(FilterDerivedEntity), typeof(FilterDerivedGraphType));
RegisterTypeMapping(typeof(DiscriminatorDerivedAEntity), typeof(DiscriminatorDerivedAGraphType));
RegisterTypeMapping(typeof(DiscriminatorDerivedBEntity), typeof(DiscriminatorDerivedBGraphType));
Query = (Query)resolver.GetService(typeof(Query))!;
Mutation = (Mutation)resolver.GetService(typeof(Mutation))!;
RegisterType(typeof(DerivedGraphType));
Expand Down