Skip to content

Fix: Guid PKs bulk-inserted as NULL without explicit ValueGeneratedNever()#105

Merged
PhenX merged 4 commits into
mainfrom
copilot/fix-bulk-insert-null-guid
May 29, 2026
Merged

Fix: Guid PKs bulk-inserted as NULL without explicit ValueGeneratedNever()#105
PhenX merged 4 commits into
mainfrom
copilot/fix-bulk-insert-null-guid

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 26, 2026

EF Core defaults Guid primary keys to ValueGenerated.OnAdd (client-side generation). The library was treating all ValueGenerated != Never properties as "database-generated" and excluding them from the INSERT column list, causing NULL to be inserted for Guid PKs unless ValueGeneratedNever() was explicitly configured.

Changes

  • ColumnMetadata.cs: IsGenerated no longer marks Guid/Guid? columns as generated unless they carry an explicit SQL default expression (GetDefaultValueSql()), are computed columns (GetComputedColumnSql()), or have a CLR-level default explicitly configured via HasDefaultValue(...) (detected via FindAnnotation(RelationalAnnotationNames.DefaultValue)). Non-Guid auto-increment columns (SQL Server IDENTITY, SQLite integer PK) are unaffected.

  • TestDbContext.cs: Removed the ValueGeneratedNever() workaround from TestEntityWithGuidId — the entity now works correctly by default.

  • BasicTestsBase.cs: Updated InsertEntities_WithGeneratedGuidId to remove the CopyGeneratedColumns = true workaround and assert that application-assigned Guid values are preserved end-to-end.

  • MergeTestsBase.cs: Updated InsertEntities_MultipleTimes_WithGuidId to assert that the application-assigned Guid Id is preserved through the merge/on-conflict path (removed .Excluding(e => e.Id) from the equivalence check).

Before / After

// This previously required .ValueGeneratedNever() in OnModelCreating — now works as-is
public class MyEntity
{
    public Guid Id { get; set; }   // assigned via Guid.NewGuid()
}

await db.ExecuteBulkInsertAsync(entities); // was: NULL inserted into Id
                                           // now:  assigned Guid preserved

A Guid column with an explicit database default (e.g. HasDefaultValueSql("NEWID()") or HasDefaultValue(someGuid)) continues to be treated as generated and excluded from INSERT by default.

Copilot AI changed the title [WIP] Fix ExecuteBulkInsertAsync for Guid primary key Fix: Guid PKs bulk-inserted as NULL without explicit ValueGeneratedNever() May 26, 2026
Copilot AI requested a review from PhenX May 26, 2026 10:21
Copilot finished work on behalf of PhenX May 26, 2026 10:21
@PhenX PhenX marked this pull request as ready for review May 29, 2026 21:49
@PhenX PhenX requested a review from Copilot May 29, 2026 21:49
Copy link
Copy Markdown
Contributor

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 bulk insert handling for EF Core’s default Guid primary key metadata by treating application-assigned Guid keys as insertable unless they have explicit database generation metadata.

Changes:

  • Refines ColumnMetadata.IsGenerated so default Guid/Guid? ValueGenerated.OnAdd columns are not automatically excluded from inserts.
  • Removes the test-only ValueGeneratedNever() workaround for TestEntityWithGuidId.
  • Updates the basic Guid key insert test to assert the assigned Guid IDs are preserved.

Reviewed changes

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

File Description
src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/ColumnMetadata.cs Updates generated-column detection for Guid/Guid? properties.
tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs Removes explicit Guid key generation override from test model configuration.
tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/Tests/Basic/BasicTestsBase.cs Adjusts Guid key bulk insert test expectations and options.

Comment on lines +25 to +27
&& (property.GetDefaultValueSql() != null
|| property.GetComputedColumnSql() != null
|| (property.ClrType != typeof(Guid) && property.ClrType != typeof(Guid?)));
Comment on lines 321 to +322
insertedEntities.Should().BeEquivalentTo(entities,
o=> o
.RespectingRuntimeTypes()
.Excluding(e => e.Id)
);
o => o.RespectingRuntimeTypes());
@PhenX
Copy link
Copy Markdown
Owner

PhenX commented May 29, 2026

@copilot take the review into account

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 29, 2026

@copilot take the review into account

Both review points are addressed:

  1. ColumnMetadata.csHasDefaultValue(...) gap (commit): The initial fix used property.GetDefaultValue() != null, but investigation showed this returns Guid.Empty for all Guid properties even without any explicit configuration — which would have broken the fix entirely. The correct check is property.FindAnnotation(RelationalAnnotationNames.DefaultValue) != null, which is only set when HasDefaultValue(...) is explicitly called in OnModelCreating. The IsGenerated condition for Guid columns is now:

    property.GetDefaultValueSql() != null       // HasDefaultValueSql("NEWID()")
    || property.GetComputedColumnSql() != null  // computed column
    || property.FindAnnotation(RelationalAnnotationNames.DefaultValue) != null  // HasDefaultValue(someGuid)

    Plain Guid PKs (client-side generation) have none of these → included in INSERT as intended.

  2. MergeTestsBase.cs — merge path coverage: Removed .Excluding(e => e.Id) from InsertEntities_MultipleTimes_WithGuidId so the merge test now asserts the full entity (including the Guid Id) is preserved after an on-conflict update.

Copilot finished work on behalf of PhenX May 29, 2026 22:06
@PhenX PhenX merged commit fea55db into main May 29, 2026
3 checks passed
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.

ExecuteBulkInsertAsync inserts NULL into Guid primary key unless ValueGeneratedNever() is configured

3 participants