Entity Framework Core: The entity type requires a primary key - A common convention error
I was recently working on another blog post on Entity Framework joins using the Fluent API when I ran into a common convention error in Entity Framework. Let’s take a quick look at what happened.
Using the same appdb example database that I used in part 1 and part 2 of my OWASP top 10 series on Broken Access Control, I set up a very simple table to store customer invoice data.
Here’s a screenshot of the table structure:
And here’s the SQL DDL for the table:
USE [appdb]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[CustomerInvoices](
[InvoiceId] [bigint] IDENTITY(1,1) NOT NULL,
[CustomerId] [int] NOT NULL,
[InvoiceAmount] [decimal](18, 2) NOT NULL,
[InvoiceMessage] [varchar](1000) NULL,
CONSTRAINT [PK_CustomerInvoices] PRIMARY KEY CLUSTERED
(
[InvoiceId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[CustomerInvoices] WITH CHECK ADD CONSTRAINT [FK_CustomerInvoices_Customers] FOREIGN KEY([CustomerId])
REFERENCES [dbo].[Customers] ([Id])
GO
ALTER TABLE [dbo].[CustomerInvoices] CHECK CONSTRAINT [FK_CustomerInvoices_Customers]
GO
So far, so good. The table has enough data to represent a simplified customer invoice. I mentioned I was working on a post about Entity Framework Core’s join syntax with the Fluent API, so I was trying to join the data to my Customers table. For a refresher, here’s a screenshot of the Customers table structure:
And here’s the SQL DDL for the Customers table:
USE [appdb]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Customers](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](50) NULL,
[Address] [varchar](250) NULL,
[City] [varchar](50) NULL,
[Region] [varchar](50) NULL,
[PostalCode] [varchar](50) NULL,
[Country] [varchar](3) NULL,
[PhoneNumber] [varchar](20) NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
And here are the model classes for Customers and CustomerInvoice:
public class Customer
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? Region { get; set; }
public string? PostalCode { get; set; }
public string? Country { get; set; }
public string? PhoneNumber { get; set; }
}
public class CustomerInvoice
{
public long InvoiceId { get; set; }
public int CustomerId { get; set; }
[Precision(18, 2)]
public decimal InvoiceAmount { get; set; }
public string? InvoiceMessage { get; set; }
public virtual Customer? Customer { get; set; }
}
After struggling a bit with the syntax for the join—my motivation for writing a blog post about joins in the first place—I wrote the following code in a console application. I’ll show you the rest of the console application when I finish the post about joins. Here’s the snippet that’s relevant to this post though:
var query = context.CustomerInvoices.Join(context.Customers,
invoice => invoice.CustomerId, customer => customer.Id,
(invoice, customer) => new
{
customer.Id,
customer.Name,
customer.Address,
invoice.InvoiceId,
invoice.InvoiceAmount,
invoice.InvoiceMessage
});
var customerInvoices = query.Where(x => x.Id == 1).ToList();
I started the console application in Visual Studio and received the following error:
System.InvalidOperationException: ‘The entity type ‘CustomerInvoice’ requires a primary key to be defined. If you intended to use a keyless entity type, call ‘HasNoKey’ in ‘OnModelCreating’. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.'
At first, this error seemed very misleading. From the screenshot and the DDL, we can see that the CustomerInvoices table definitely has a primary key defined. So, what’s going on here? Why am I getting an InvalidOperationException that says my table doesn’t have a primary key? It took some time to figure this out and I had to look up the answer. I’ve run into issues like this before, so in the spirit of this blog, which is to reinforce my knowledge while also helping others, here’s an important principle to keep in mind when working with any version of Entity Framework:
This error, and others like it, don’t occur because of a problem in the database structure—They occur because there is a disconnect between what the structure of the database actually is and what the code is telling Entity Framework the structure of the database should be.
The answer to why this error is occurring can be found under the Keys page in the Entity Framework Documentation Hub .
On that page, we find the following statement under the “Configuring a primary key” heading:
“By convention, a property named Id or <type name>Id will be configured as the primary key of an entity.”
Here’s the primary key for our CustomerInvoices table again:
[InvoiceId] [bigint] IDENTITY(1,1) NOT NULL,
And here’s the CustomerInvoice model again:
public class CustomerInvoice
{
public long InvoiceId { get; set; }
public int CustomerId { get; set; }
[Precision(18, 2)]
public decimal InvoiceAmount { get; set; }
public string? InvoiceMessage { get; set; }
public virtual Customer? Customer { get; set; }
}
The problem is that the name of the InvoiceId property doesn’t match the naming convention for what Entity Framework will automatically consider the primary key of the table. So, Entity Framework can’t find a primary key using the convention established for the framework. It’s looking for a property called either Id, which would map to a column in the database called Id, or a column called CustomerInvoiceId, which follows the <TypeName>Id convention, where <TypeName> is the name of the C# class. In our case, the class is CustomerInvoice, so EF Core looks for a property named CustomerInvoiceId, not InvoiceId. This table doesn’t use either of those, so Entity Framework doesn’t recognize any of the properties as the primary key.
💡 EF Core Convention Reminder:
EF Core will automatically recognize a property as the primary key if it is named Id or <TypeName>Id, where <TypeName> is your C# class name, not the table name in your database.
There are a few ways to fix this. Which option is best depends on the state of your application. Here is a list of potential resolutions.
- Rename the column to match one of the established Entity Framework naming conventions, either Id or CustomerInvoiceId for this example.
Pros: Uses the same name for the column in your database schema and application source code. Cons: Renaming a column is often impractical if the table is already in use. You would have to update every reference to the column in your codebase. Depending on the size and complexity of your application and the larger system in which it might live, this might be a very high-risk change.
- Use the Key attribute.
Entity Framework provides an attribute called [Key] that can be used to specifically tell Entity Framework which column should be the primary key. We can gain access to this attribute by adding the following using statement:
using System.ComponentModel.DataAnnotations;
Once we have access, we can add the attribute to the model class like this:
public class CustomerInvoice
{
[Key]
public long InvoiceId { get; set; }
public int CustomerId { get; set; }
[Precision(18, 2)]
public decimal InvoiceAmount { get; set; }
public string? InvoiceMessage { get; set; }
public virtual Customer? Customer { get; set; }
}
Pros: Simple Cons: Attributes may not provide enough flexibility for more complicated scenarios.
- Use the fluent API
We can add code using the Fluent API that tells Entity Framework what the primary key should be. The simplest way to do this would be to override the OnModelCreating method of the database context object, if you haven’t done so already, and then add code like the following:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CustomerInvoice>()
.HasKey(c => c.InvoiceId);
}
The above code tells Entity Framework that InvoiceId is the primary key for the CustomerInvoice entity.
While this is the simplest way to add code-based configuration, putting every table in a system into the OnModelCreating method of the context object can become unwieldy for more complex applications. For more complicated applications that may have hundreds of tables to define, the preferred approach is to use entity type configuration classes.
To use entity type configuration classes, we would define a map for our model that extends the IEntityTypeConfiguration interface. Here’s an example for the CustomerInvoice class:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class CustomerInvoiceMap : IEntityTypeConfiguration<CustomerInvoice>
{
public void Configure(EntityTypeBuilder<CustomerInvoice> builder)
{
builder.ToTable("CustomerInvoices");
builder.HasKey(c => c.InvoiceId);
}
}
We would then apply the map in the context class’s OnModelCreating method like this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new CustomerInvoiceMap());
}
If you have multiple classes, the configurations can all be applied at once using the following:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
Just keep in mind that “AppDbContext” is the name of my context class. So where you see “AppDbContext” in the example line, you would need to replace that with the name of your actual context class. Here’s a version of that line written more clearly:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(YOURCONTEXTCLASSNAMEHERE).Assembly);
}
It may not seem much different than just putting the table mapping directly into the OnModelCreating method, but for hundreds of tables that may have complex configurations, it can make the code much easier to read and maintain.
Pros: Extremely flexible and customizable, especially for complex applications. Cons: Adds additional complexity to the application.
Whichever option you choose, it’s best to maintain consistency throughout your application. So, if you choose to use attributes like [Key], use those consistently in your application source code. If you choose to use Fluent API, use that consistently. Avoid mixing the two. Mixing attributes and fluent API can lead to maintenance issues or unexpected overrides, so it’s best to stick with one configuration approach consistently throughout your application.
You may have noticed earlier that my CustomerInvoice model is using the [Precision] attribute to define the precision of the decimal InvoiceAmount column.
[Precision(18, 2)]
public decimal InvoiceAmount { get; set; }
Since we don’t want to mix attributes and configuration, let’s remove that from the CustomerInvoice class and put it into the map we defined earlier:
Here’s the updated CustomerInvoice class, now free of attributes:
public class CustomerInvoice
{
public long InvoiceId { get; set; }
public int CustomerId { get; set; }
public decimal InvoiceAmount { get; set; }
public string? InvoiceMessage { get; set; }
public virtual Customer? Customer { get; set; }
}
And here’s the updated CustomerInvoiceMap class:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class CustomerInvoiceMap : IEntityTypeConfiguration<CustomerInvoice>
{
public void Configure(EntityTypeBuilder<CustomerInvoice> builder)
{
builder.ToTable("CustomerInvoices");
builder.HasKey(c => c.InvoiceId);
builder.Property(p => p.InvoiceAmount)
.HasPrecision(18, 2);
}
}
Or, if you just want to put everything in the OnModelCreating method of the context class, you can do so like this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CustomerInvoice>()
.HasKey(c => c.InvoiceId);
modelBuilder.Entity<CustomerInvoice>()
.Property(c => c.InvoiceAmount)
.HasPrecision(18, 2);
}
In this post, we’ve looked at how EF Core uses conventions to determine the primary key of an entity, and how to explicitly specify the key when those conventions aren’t followed.
The content on this blog is for informational and educational purposes only and represents my personal opinions and experience. While I strive to provide accurate and up-to-date information, I make no guarantees regarding the completeness, reliability, or accuracy of the information provided.
By using this website, you acknowledge that any actions you take based on the information provided here are at your own risk. I am not liable for any losses, damages, or issues arising from the use or misuse of the content on this blog.
Please consult a qualified professional or conduct your own research before implementing any solutions or advice mentioned here.