Skip to content

Fix complex property JSON column not marked nullable in TPH hierarchy#37781

Open
Copilot wants to merge 5 commits intorelease/10.0from
copilot/fix-json-property-nullability
Open

Fix complex property JSON column not marked nullable in TPH hierarchy#37781
Copilot wants to merge 5 commits intorelease/10.0from
copilot/fix-json-property-nullability

Conversation

Copy link
Contributor

Copilot AI commented Feb 24, 2026

In a TPH (Table Per Hierarchy) mapping, columns exclusive to a derived entity type must be nullable because rows belonging to sibling derived types will not have a value for those columns. This was correctly handled for regular columns and owned entity JSON columns, but not for complex property JSON columns. As a result, databases would raise a not-null constraint violation when saving an instance of a sibling derived type.

Changes Made

  • RelationalModel.CreateContainerColumn: When computing IsNullable for a complex property JSON column, added a check that the complex property's declaring entity type has a base type and is using TPH mapping strategy (null strategy defaults to TPH). The check is evaluated before the chain.Any(p => p.IsNullable) check so that TPH-derived types are correctly identified before inspecting the chain.
  • RelationalModelTest: Added unit test Complex_property_json_column_is_nullable_in_TPH_hierarchy verifying that the JSON container column is marked nullable when a complex property is defined on a derived type in a TPH hierarchy.
  • CompiledModelSqlServerTest.ComplexTypes baseline (DbContextModelBuilder.cs): Updated to reflect that the ManyOwned JSON column (declared on the derived PrincipalDerived type in a TPH hierarchy) is now correctly marked as nullable.

Testing

  • ✅ New unit test Complex_property_json_column_is_nullable_in_TPH_hierarchy validates the fix
  • ✅ All existing RelationalModelTest tests continue to pass
  • ✅ The TPH strategy check ensures TPT and TPC hierarchies are unaffected
  • CompiledModelSqlServerTest.ComplexTypes baseline updated to match the corrected nullability
Original prompt

This section details on the original issue you should resolve

<issue_title>Complex property stored as json will be marked non-nullable even in TPH class hierarchy</issue_title>
<issue_description>### Bug description

When two subclasses are stored in the same table using TPH, all columns mapped from properties that are exclusive to one of the subclasses will be marked as nullable, becuase if the row is an instance of the 'other' subclass that column will not have a value. Except for complex properties stored as json. This leads to exceptions when storing data because the database expect a value where there is none. This issue does not exist when using owned entities instead of complex entities.

Workaround is to explicitly specifiy IsRequired(false) but that doesn't seem the intended solution.

Your code

using System.Diagnostics;
using Microsoft.EntityFrameworkCore;

const string connectionString = "Server=localhost;Database=complex-issues;Port=5432;User Id=postgres;Password=postgres;Include Error Detail=true";
var options = new DbContextOptionsBuilder<AppDbContext>()
  .UseNpgsql(connectionString)
  .Options;
var dbContext = new AppDbContext(options);
await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();

var entity = new EntityWithoutComplexJson();
dbContext.Entities.Add(entity);
await dbContext.SaveChangesAsync();

public abstract class Entity
{
  public Guid Id { get; init; }
}

public class EntityWithoutComplexJson : Entity
{
  
}

public class EntityWithComplexJson : Entity
{
  public required ComplexEntity ComplexEntity { get; init; }
}

public class ComplexEntity
{
  public required string Value { get; init; }
}

public class AppDbContext(DbContextOptions options) : DbContext(options)
{
  public DbSet<Entity> Entities { get; init; }
  
  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<EntityWithoutComplexJson>();
    modelBuilder.Entity<EntityWithComplexJson>().ComplexProperty(entity => entity.ComplexEntity).ToJson();
  }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    base.OnConfiguring(optionsBuilder);
    optionsBuilder.LogTo(message =>
    {
      Debug.WriteLine(message);
    });
  }
}

Stack traces

---> Npgsql.PostgresException (0x80004005): 23502: null value in column "ComplexEntity" of relation "Entities" violates not-null constraint

DETAIL: Failing row contains (019b373e-f53a-7786-ac07-94575699cf3c, EntityWithoutComplexJson, null).
   at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
   at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
   at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
  Exception data:
    Severity: ERROR
    SqlState: 23502
    MessageText: null value in column "ComplexEntity" of relation "Entities" violates not-null constraint
    Detail: Failing row contains (019b373e-f53a-7786-ac07-94575699cf3c, EntityWithoutComplexJson, null).
    SchemaName: public
    TableName: Entities
    ColumnName: ComplexEntity
    File: execMain.c
    Line: 1947
    Routine: ExecConstraints
   --- End of inner exception stack trace ---

Verbose output


EF Core version

10.0.1

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

.NET 10.0

Operating system

MacOS

IDE

Rider</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix complex property nullability in TPH hierarchy Fix complex property JSON column not marked nullable in TPH hierarchy Feb 24, 2026
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
… check

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@AndriySvyryd
Copy link
Member

@copilot Update the PR description with a nice summary, referencing the issue being fixed

Copy link
Contributor Author

Copilot AI commented Feb 26, 2026

@copilot Update the PR description with a nice summary, referencing the issue being fixed

Done — updated the PR description with a summary referencing the issue, following the servicing PR template (Description, Customer impact, How found, Regression, Testing, Risk).

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where complex properties stored as JSON columns were incorrectly marked as non-nullable in Table-Per-Hierarchy (TPH) inheritance scenarios. In TPH mapping, all entity types in the hierarchy share the same database table, so columns exclusive to derived types must be nullable to accommodate rows of other sibling types.

Changes:

  • Added null-checking logic in RelationalModel.CreateContainerColumn to mark JSON columns as nullable when the complex property is declared on a derived entity type using TPH mapping strategy
  • Added unit test Complex_property_json_column_is_nullable_in_TPH_hierarchy to verify the fix

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/EFCore.Relational/Metadata/Internal/RelationalModel.cs Added TPH-specific nullability check for complex property JSON columns to match owned entity behavior
test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs Added test with TPH base entity and two derived types (one with, one without complex property) to verify JSON column is nullable

@AndriySvyryd AndriySvyryd marked this pull request as ready for review February 26, 2026 02:24
@AndriySvyryd AndriySvyryd requested a review from a team as a code owner February 26, 2026 02:24
@AndriySvyryd
Copy link
Member

@copilot Also update CompiledModelSqlServerTest.ComplexTypes baseline

…in TPH hierarchy

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 26, 2026

@copilot Also update CompiledModelSqlServerTest.ComplexTypes baseline

Done in d908ec6. Updated DbContextModelBuilder.cs in the ComplexTypes baseline to add IsNullable = true for the manyOwnedColumn (the ManyOwned JSON column on PrincipalDerived, which is a derived type in a TPH hierarchy).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants