Skip to main content

How to Set Table Names in EF Core with ToTable (Lowercase for MySQL)

· 7 min read
Jagdish Kumawat
Founder @ Dewiride

EF Core names each table after your DbSet property: Products, Categories. On SQL Server or Windows that's harmless, but MySQL on Linux and in Docker compares table names case-sensitively, so products and Products are different tables and hand-written SQL breaks. This guide sets lowercase names with ToTable, shows the rename migration EF Core generates for an existing database, and the SQL MySQL actually runs.

Where Table Names Come From​

EF Core's entity types page gives the rule: "By convention, each entity type will be set up to map to a database table with the same name as the DbSet property that exposes the entity. If no DbSet exists for the given entity, the class name is used."

With this DbContext:

src/Dewiride.Inventory.Data/InventoryDbContext.cs (excerpt)
public DbSet<Category> Categories => Set<Category>();

public DbSet<Product> Products => Set<Product>();

the first migration created tables called Categories and Products.

Why Lowercase Matters on MySQL​

MySQL's identifier case sensitivity page says table names "are not case-sensitive in Windows, but are case-sensitive in most varieties of Unix". The setting behind it, lower_case_table_names, was 0 on this MySQL 9.7 in Docker, which runs Linux:

mysql>
SELECT @@lower_case_table_names;
Output
+--------------------------+
| @@lower_case_table_names |
+--------------------------+
| 0 |
+--------------------------+
1 row in set (0.000 sec)

With 0, "Name comparisons are case-sensitive". Lowercase names avoid the whole question: every platform and every person writing SQL by hand gets products right.

Prerequisites​

Step 1: Set the Names with ToTable​

In each entity's configuration, or in OnModelCreating:

src/Dewiride.Inventory.Data/Configurations/CategoryConfiguration.cs (excerpt)
builder.ToTable("categories");
src/Dewiride.Inventory.Data/Configurations/ProductConfiguration.cs (excerpt)
builder.ToTable("products");

The same can be written as an attribute on the class, [Table("products")]; the docs show both. ToTable keeps database names out of your entity classes, next to the rest of the mapping.

Step 2: Read the Rename Migration​

The tables already exist, so EF Core generates a rename:

Terminal
dotnet ef migrations add LowercaseTableNames --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Migrations/…_LowercaseTableNames.cs (Up)
migrationBuilder.DropForeignKey(
name: "FK_Products_Categories_CategoryId",
table: "Products");

migrationBuilder.DropPrimaryKey(
name: "PK_Products",
table: "Products");

migrationBuilder.DropPrimaryKey(
name: "PK_Categories",
table: "Categories");

migrationBuilder.RenameTable(
name: "Products",
newName: "products");

migrationBuilder.RenameTable(
name: "Categories",
newName: "categories");

migrationBuilder.RenameIndex(
name: "IX_Products_CategoryId",
table: "products",
newName: "IX_products_CategoryId");

migrationBuilder.AddPrimaryKey(
name: "PK_products",
table: "products",
column: "Id");

migrationBuilder.AddPrimaryKey(
name: "PK_categories",
table: "categories",
column: "Id");

migrationBuilder.AddForeignKey(
name: "FK_products_categories_CategoryId",
table: "products",
column: "CategoryId",
principalTable: "categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);

More than a rename: the names of the primary keys, the index and the foreign key all contain the table name, so EF Core renames them too. Dropping and re-adding a primary key on a table with an AUTO_INCREMENT column looks risky on MySQL, so read the SQL before applying anything:

Terminal
dotnet ef migrations script CreateCatalogTables LowercaseTableNames --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Output
START TRANSACTION;
ALTER TABLE `Products` DROP CONSTRAINT `FK_Products_Categories_CategoryId`;

ALTER TABLE Products RENAME products;

ALTER TABLE Categories RENAME categories;

ALTER TABLE `products` RENAME INDEX `IX_Products_CategoryId` TO `IX_products_CategoryId`;

ALTER TABLE `products` ADD CONSTRAINT `FK_products_categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `categories` (`Id`) ON DELETE CASCADE;

INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20261103043533_LowercaseTableNames', '10.0.14');

COMMIT;

A terminal where dotnet ef migrations script for LowercaseTableNames shows the SQL: drop the foreign key, rename Products and Categories to lowercase, rename the index, and add the foreign key back, with no primary key statements.

The primary-key steps are gone. MySQL calls every primary key PRIMARY, whatever a tool names it, so Oracle's provider leaves the drop and re-add out of the SQL. The data is never touched: RENAME changes only the name.

Step 3: Apply It​

Terminal
dotnet ef database update --project src/Dewiride.Inventory.Data --startup-project src/Dewiride.Inventory.Api
Output (the lines that matter)
Applying migration '20261103043533_LowercaseTableNames'.
ALTER TABLE `Products` DROP CONSTRAINT `FK_Products_Categories_CategoryId`;
ALTER TABLE Products RENAME products;
ALTER TABLE Categories RENAME categories;
ALTER TABLE `products` RENAME INDEX `IX_Products_CategoryId` TO `IX_products_CategoryId`;
ALTER TABLE `products` ADD CONSTRAINT `FK_products_categories_CategoryId` FOREIGN KEY (`CategoryId`) REFERENCES `categories` (`Id`) ON DELETE CASCADE;
Done.
mysql>
SHOW TABLES;
Output
+-----------------------+
| Tables_in_inventory |
+-----------------------+
| __EFMigrationsHistory |
| categories |
| products |
+-----------------------+
3 rows in set (0.001 sec)

Step 4: Know What Breaks​

Code using EF Core keeps working, because EF Core always uses the names from the model. SQL written by hand, reports, and other tools that still say Products don't:

mysql>
SELECT COUNT(*) FROM Products;
SELECT COUNT(*) FROM products;
Output
ERROR 1146 (42S02): Table 'inventory.Products' doesn't exist
+----------+
| COUNT(*) |
+----------+
| 1 |
+----------+
1 row in set (0.000 sec)

The mysql client where SHOW TABLES lists categories and products in lowercase, SELECT COUNT(*) FROM Products fails with ERROR 1146 Table 'inventory.Products' doesn't exist, and the lowercase name works.

Rename tables early, before anything else depends on the old names. Fixed: ERROR 1146 Table doesn't exist explains the error in detail.

Common Mistakes​

MistakeWhat happensDo this instead
Keeping EF Core's default names on MySQLCase-sensitive names that hand-written SQL gets wrongSet lowercase names with ToTable from the first migration
Renaming a table in MySQL by handThe model and the database disagree; the next migration tries againRename through the model and a migration
Renaming after reports and scripts existThey break with error 1146Rename early, and update every consumer
Mixing naming styles across tablesNobody can guess a table's namePick one style and use it everywhere

FAQ​

Do column names need to be lowercase too?​

No. MySQL compares column names case-insensitively on every platform; the manual says column names "are not case-sensitive on any platform". These tables keep EF Core's PascalCase column names.

Can I lowercase every table automatically?​

Yes, by looping over the model in OnModelCreating and calling SetTableName for each entity. Naming each table explicitly with ToTable is clearer, and it keeps the names visible next to each entity's mapping.

Conclusion​

EF Core names tables after the DbSet properties, and MySQL on Linux treats Products and products as different tables. builder.ToTable("products") sets a lowercase name; on an existing database, EF Core generates a rename that MySQL runs as ALTER TABLE … RENAME, without touching data. Rename early: EF Core follows the model, but hand-written SQL that uses the old name fails with error 1146.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.