Skip to content

Settings.ForeignKeyName

Simon Hughes edited this page Aug 30, 2026 · 1 revision

Settings.ForeignKeyName

Names the navigation properties a foreign key produces, and is called repeatedly until the name it returns is unique.

Type Func<string, ForeignKey, string, Relationship, short, string>
Parameters (tableName, foreignKey, foreignKeyName, relationship, attempt)
Default A set of rules that strip an Id suffix, fall back to the table name, and add a number as a last resort
Applies to EF 6 and EF Core
Databases All
In Database.tt? Yes, and it is the longest block in the file

What it does

A foreign key produces up to two navigation properties: one on the child pointing at the parent (Product.Category) and one on the parent holding the children (Category.Products). Naming them is surprisingly hard, because the obvious name is frequently taken - by the table's own name, by one of its columns, or by another foreign key to the same table.

So the generator does not ask once. It calls this callback, checks whether the answer collides with anything already used on that table, and if it does, calls again with a higher attempt number. Your job is to return a name; its job is to keep asking until one sticks.

attempt What the generator is trying
1 The foreign key column name, PascalCased - CategoryId
2 The same with a trailing Id stripped - Category. Only tried when the name ends in id
3 Another go on the child side, for the awkward cases
4 The parent table name
5 The same again, with 1, 2, 3… appended until one is free
6 Whatever you return is used, collision or not

The numbers are part of the public contract and are never renumbered, so branching on attempt is safe.

The other parameters: tableName is the other end's name (already singular or plural as appropriate), foreignKeyName is the candidate the generator built, relationship is OneToMany, ManyToOne, OneToOne or ManyToMany, and foreignKey gives you FkColumn, PkColumn, ConstraintName, FkTableName and PkTableName for matching.

Example

Renaming one relationship by matching on its column:

Settings.ForeignKeyName = delegate(string tableName, ForeignKey foreignKey, string foreignKeyName, Relationship relationship, short attempt)
{
    if (foreignKey.FkColumn == "CategoryId" && attempt == 1)
        return "Group";

    return tableName;
};

Before

    // Product
    public class Product
    {
        public int ProductId { get; set; } // ProductId (Primary key)
        public string ProductName { get; set; } // ProductName (length: 100)
        public decimal UnitPrice { get; set; } // UnitPrice
        public string Notes { get; set; } // Notes
        public int CategoryId { get; set; } // CategoryId
        public string DisplayLabel { get; private set; } // DisplayLabel (length: 150)

        // Foreign keys

        /// <summary>
        /// Parent Category pointed by [Product].([CategoryId]) (FK_Product_Category)
        /// </summary>
        public Category Category { get; set; } // FK_Product_Category

        public Product()
        {
            UnitPrice = 0m;
        }
    }

After

    // Product
    public class Product
    {
        public int ProductId { get; set; } // ProductId (Primary key)
        public string ProductName { get; set; } // ProductName (length: 100)
        public decimal UnitPrice { get; set; } // UnitPrice
        public string Notes { get; set; } // Notes
        public int CategoryId { get; set; } // CategoryId
        public string DisplayLabel { get; private set; } // DisplayLabel (length: 150)

        // Foreign keys

        /// <summary>
        /// Parent Category pointed by [Product].([CategoryId]) (FK_Product_Category)
        /// </summary>
        public Category Group { get; set; } // FK_Product_Category

        public Product()
        {
            UnitPrice = 0m;
        }
    }

Product.Category became Product.Group. The XML comment still names the real constraint, so the mapping back to the database is never lost.

When to use it

Encoding the names in the constraint. If your team names constraints FK_Child_ParentNavName_ChildNavName, you can drive both navigation property names from the database:

Settings.ForeignKeyName = delegate(string tableName, ForeignKey fk, string fkName, Relationship relationship, short attempt)
{
    var parts = fk.ConstraintName.Split('_');
    if (fk.ConstraintName.StartsWith("FK_") && parts.Length == 4 && parts[1] == fk.FkTableName)
    {
        if (relationship == Relationship.OneToMany) return parts[3];
        if (relationship == Relationship.ManyToOne) return parts[2];
    }

    return tableName;
};

Self-referencing keys, where the default names are rarely what you want. Employee.ReportsTo reads far better as Employee.Manager:

if (fk.FkTableName == "Employee" && fk.FkColumn == "ReportsTo")
    return "Manager";

Two foreign keys to the same table. Order.ShipToAddress and Order.BillToAddress instead of Address and Address1.

Gotchas

Always return something. Returning null or an empty string generates a property with no name, which does not compile. When your rule does not apply, return tableName or foreignKeyName rather than falling off the end.

Guard on attempt or you will fight the collision resolver. Returning the same constant for every attempt means the generator asks five times, gets the same taken name each time, and lands on attempt 6 - which it uses regardless of the collision. Either branch on attempt == 1 as above, or make sure your name is genuinely unique.

It is called for both ends of the relationship. relationship tells you which end you are naming; ignoring it renames both.

A name you supply through Settings.AddExtraForeignKeys wins over this callback. ParentName and ChildName on a manually added relationship are used verbatim and this is never consulted for them.

Renaming does not change the mapping. HasForeignKey still points at the real column; only the C# property name moves.

See also

Clone this wiki locally