Context
I'm working on a project that has Brands and OemModels. There is a one-to-many relationship between them. A Brand can have any number of OemModels, but an OemModel can only ever have 1 brand. For business reasons, OemModels need to be aware of what Brand they belong to.
The project is built using Entity Framework Core 5.0, and the Npgsql 5.0.1 package to connect to a PostgreSQL database. I'm using a code-first approach to generate the database.
This project is also using the optional Nullable Reference Types feature. This means that any navigation property that is pointing to a reference type is essentially [Required], as per the MSDN documentation:
If nullable reference types are enabled, properties will be configured based on the C# nullability of their .NET type:
string?will be configured as optional, butstringwill be configured as required.
Here is my database context class:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
public DbSet<Brand> Brands => Set<Brand>();
public DbSet<OemModel> OemModels => Set<OemModel>();
}
Here are my Model classes:
public class Brand
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public List<OemModel> OemModels { get; set; } = new();
}
public class OemModel
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string OemNumber { get; set; } = string.Empty;
public int BrandId { get; set; }
public Brand Brand { get; set; } = null!;
}
I am using the "null!" null forgiving operator as per the MSDN documentation:
As a terser alternative, it is possible to simply initialize the property to null with the help of the null-forgiving operator (!):
public Product Product { get; set; } = null!;An actual null value will never be observed except as a result of a programming bug, e.g. accessing the navigation property without properly loading the related entity beforehand.
My problem
I am currently having issues when adding models to existing brands in my integration tests.
Assuming the following code already ran successfully:
var firstBrand = new Brand{ Name = "FirstBrand" };
var secondBrand = new Brand{ Name = "SecondBrand" };
_context.Brands.Add(firstBrand);
_context.Brands.Add(secondBrand);
await _context.SaveChangesAsync();
I am unable to add models to these newly created brands:
var firstModel = new Model
{
Name = "FirstModel",
OemNumber = "A-1"
};
var secondModel = new Model
{
Name = "SecondModel",
OemNumber = "A-2"
};
var thirdModel = new Model
{
Name = "ThirdModel",
OemNumber = "B-1"
};
firstBrand.OemModels.Add(firstModel);
firstBrand.OemModels.Add(secondModel);
secondBrand.OemModels.Add(thirdModel);
await _context.SaveChangesAsync();
Note: Both code blocks above are in the same method, without any code between them.
The block of code above fails with the following error message:
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
Inner exception:
Npgsql.PostgresException
23505: duplicate key value violates unique constraint "PK_Brands"
at Npgsql.NpgsqlConnector.<ReadMessage>g__ReadMessageLong|194_0(NpgsqlConnector connector, Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
I have also tried to explicitly define the Brand and BrandId properties in the models (which I thought made sense, since they are essentially [Required] before of the NRT setting). For example:
var firstModel = new OemModel
{
Name = "FirstModel",
OemNumber = "A-1",
Brand = firstBrand,
BrandId = firstBrand.Id
};
But it still fails with the same error.
I find this confusing, since my database migration generates the following for my model tables:
migrationBuilder.CreateTable(
name: "OemModels",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy",NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
OemNumber = table.Column<string>(type: "text", nullable: false),
BrandId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OemModels", x => x.Id);
table.ForeignKey(
name: "FK_OemModels_Brands_BrandId",
column: x => x.BrandId,
principalTable: "Brands",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
And:
migrationBuilder.CreateTable(
name: "Brands",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy",NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Brands", x => x.Id);
});
The OemModel table doesn't have a "PK_Brands", but the exception is raised when I try to save the changes made when adding OemModels to existing brands.
In the debugger, both of my Brands have unique Ids after they're added to the context (as I'd expect), so they're both unique at the time of adding the OemModels to their internal lists. And my understanding is that the models should have their Ids generated automatically if I add them to existing brands and save changes, no?
I'm kind of confused on what the right way of adding OemModels to existing Brands is if the principal Brand is a [Required] property. The way I understand it, the null!; assignment should allow me to create OemModels without specifying a navigation property for its parent Brand when adding it directly to the Brand's list of OemModels, but I get the "PK_Brand" error regardless of if they're defined or not.