4

We're in the process of migrating our moderately large application from Entity Framework 6.2 to Entity Framework Core 2.0, and while many things have been easier than I had dared to hope for, I've run into a problem that I doubt I'm the first (or the thousandth) one to have, but can't find a compact solution for it: Naming conventions.

Specifically, naming conventions for relational properties.

A typical one-to-many entity pair in our application looks something like this:

public class Foo
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int FooId { get; set; }

    [Required]
    public string Bar { get; set; }

    [InverseProperty(nameof(Baz.Foo))]
    public ICollection<Baz> Bazzes { get; set; }
}

public class Baz
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int BazId { get; set; }

    [Required]
    public string Qux { get; set; }

    [Required]
    public Foo Foo { get; set; }
}

In our existing database, EF6 mapped that to the following tables:

Foos                        Bazzes -- named after the DbSets on the DbContext
****                        ******
FooId : int                 BazId : int
Bar : nvarchar(max)         Qux : nvarchar(max)
                            Foo_FooId : int

With the default naming conventions in EF Core, it seems that the ported context looks for a column FooId instead of Foo_FooId.

I've tried modifying this with something what's described here, but I can't figure out what condition to impose on the property metadata to only change the navigational properties. (I also have a couple of complex type, which I configure with .Entity<Owner>().OwnsOne(o => o.Owned), so the solution needs to capture that as well.)

Is there no ready-made solution to make this transition easier?


I've published an MVCE for this problem here: https://github.com/tlycken/MCVE_EfCoreNavigationalProperties

Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402
  • As far as I'm aware, no. When we migrated (to EF Core 1.0) we scaffolded the database and adjusted the code accordingly. I can't find any posts that suggest this has changed for the better in any way. – Tubs Dec 06 '17 at 16:53

2 Answers2

3

AFAIK you can't change the name of the Shadow Property, but you can change the name of the table column. Like this

protected override void OnModelCreating(ModelBuilder modelBuilder)
{

    base.OnModelCreating(modelBuilder);

    //modelBuilder.Model.FindEntityType(typeof(Baz)).FindProperty("FooId").Relational().ColumnName = "Foo_FooId";
    foreach( var m in modelBuilder.Model.GetEntityTypes())
    {
        foreach (var sp in m.GetProperties().Where( p => p.IsShadowProperty ))
        {
            var fk = sp.GetContainingForeignKeys().Where(f => f.Properties.Count == 1).FirstOrDefault();
            if (fk != null && fk.PrincipalKey.Properties.Count == 1)
            {
                sp.Relational().ColumnName = fk.PrincipalEntityType.Relational().TableName + "_" + fk.PrincipalKey.Properties.Single().Relational().ColumnName;
            }
        }
    }

}

Of course if you have a Foreign Key Property in your model, you wouldn't have to do this.

David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • Thanks. I've been able to play around with this now, but I'm unable to get it working, for a couple of reasons: Your code assumes that the prefix is the table name of the other entity, but in fact it should be the property name on _this_ entity (e.g. if we have an entity of type `Foo` with a navigational property of type `Bar` named `Baz`, the column in the `Foos` table should be `Baz_Bar`) and I can't figure out how to find the name of the property. – Tomas Aschan Dec 12 '17 at 10:49
  • I've also tried annotating the navigational property with `[ForeignKey("TheDesiredColumnName")]`, which seems to make the name resolve correctly, but `Include` stops working (I get `null` for all relational parameters). There is a foreign key in the database, but that doesn't seem to help. – Tomas Aschan Dec 12 '17 at 10:50
  • Also, I put together a minimal sample app that demostrates the problem here: https://github.com/tlycken/MCVE_EfCoreNavigationalProperties – Tomas Aschan Dec 12 '17 at 10:53
  • 1
    Like this? `sp.Relational().ColumnName = fk.DependentToPrincipal.Name + "_" + fk.PrincipalKey.Properties.Single().Relational().ColumnName;`? – David Browne - Microsoft Dec 12 '17 at 14:26
0

Here's how I managed to re-create the naming conventions for EF6 in EF Core 5.

FK name from TableNameId to TableName_Id

The most common scenario (AFAIK) is when your primary keys are simply set as Id in your models, as in:

public class Foo
{
    public int Id { get; set; }
    ...
}

The EF Core 5 default column name for foreign keys is TableNameId (e.g. FooId). To create them in EF6 style TableName_Id (e.g. Foo_Id), here is the configuration:

public class TestContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }
    public DbSet<Baz> Bazzes { get; set; }

    // ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        AddConfigurations(modelBuilder);

        RenameForeignKeys(modelBuilder);

        base.OnModelCreating(modelBuilder);
    }

    private static void AddConfigurations(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfiguration(new FooConfiguration());
    }

    private static void RenameForeignKeys(ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            var properties = entityType.GetProperties()
                .Where(property => property.IsForeignKey() && !property.DeclaringEntityType.IsOwned());

            foreach (var property in properties)
            {
                var newName = GetNewColumnName(property.Name);

                property.SetColumnName(newName);
            }
        }
    }

    private static string GetNewColumnName(string name)
    {
        if (name.Length > 2 && name[^2..] == "Id" && name[^3..] != "_Id")
        {
            return $"{name[0..^2]}_Id";
        }

        return name;
    }
}

public class FooConfiguration : IEntityTypeConfiguration<Foo>
{
    public void Configure(EntityTypeBuilder<Foo> builder)
    {
    }
}

FK name from TableNameId to TableName_TableNameId

In the OP's case, the primary keys are set as TableNameId (e.g. FooId) in the model. In order to create foreign keys as TableName_TableNameId (e.g. Foo_FooId), just replace these two methods from the previous example:

private static void RenameForeignKeys(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        var properties = entityType.GetProperties()
            .Where(property => property.IsForeignKey() && !property.DeclaringEntityType.IsOwned());

        foreach (var property in properties)
        {
            var foreignKey = property.GetContainingForeignKeys().Single();

            var principalEntityName = foreignKey.PrincipalEntityType.FullName();

            var newName = GetNewColumnName(property.Name, principalEntityName);

            property.SetColumnName(newName);
        }
    }
}

private static string GetNewColumnName(string name, string tableName)
{
    if (name.Length > 2 && name[^2..] == "Id" && !name.Contains("_"))
    {
        return $"{name[0..^2]}_{tableName}Id";
    }

    return name;
}

Notes:

  • I'm using .Single() in property.GetContainingForeignKeys().Single() since I didn't find any cases where that method didn't return only one entry.
  • !name.Contains("_") is there to prevent overwriting any foreign key configurations you might have set manually.
Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62