Search code examples
entity-framework-coremany-to-many

ef core: unidirectional many-to-many relationships


EF Core beginner here, so please be patient. We're trying to migrate an existing app from NHibernate to EF Core and we're facing several interesting issues. One of them is related with unidirectional many-to-many relationships.

According to the docs, EF supports unidirection many-to-many relationships. Unfortunately, I'm having some issues when trying to configure it. Here's a snippet that presents the classes we need to map into tables:

// this is the class that has the reference to the other entity
public class GuiaAbate : Entity 
{
    private IList<Equipamento> _equipamentos = new List<Equipamento>( );

  // other properties/methods removed

   // unidirectional navigation to many
   public IEnumerable<Equipamento> Equipamentos {
        get => new ReadOnlyCollectionBuilder<Equipamento>(_equipamentos);
        internal set => _equipamentos = value.ToList( );
    }

}

// even though I believe it's not that important, here's the other class
// this is an abstract class since there are several derived classes
// for this type, all derived types are mapped to the same table
public abstract class Equipamento : Entity, IVersionamento 
{
  // it has some properties and collections which reference other entiites
  // however, there's nothing point back to GuiaAbate (ie, no IEnumerable<GuiaAbate> prop)
  // because we're only interested in the other side of the navigation
}

We can't change the database tables, so here's what it looks like now:

CREATE TABLE [dbo].[GuiaAbate](
    [IDGuiaAbate] [int] IDENTITY(1,1) NOT NULL,
    [Data] [datetime] NOT NULL,
    [Login] [varchar](50) NOT NULL,
    [LoginAD] [int] NOT NULL,
 CONSTRAINT [PK_GuiaAbate] PRIMARY KEY CLUSTERED 
(
    [IDGuiaAbate] ASC
) ) ON [PRIMARY]


CREATE TABLE [dbo].[Equipamento](
    [IDEquipamento] [int] IDENTITY(1,1) NOT NULL,

    /* other fields removed */ 

 CONSTRAINT [PK_Equipamento] PRIMARY KEY CLUSTERED 
(
    [IDEquipamento] ASC
)) ON [PRIMARY]



CREATE TABLE [dbo].[GuiaAbateEquipamento](
    [IDGuiaAbateEquipamento] [bigint] IDENTITY(1,1) NOT NULL,
    [IDGuiaAbate] [int] NOT NULL,
    [IDEquipamento] [int] NOT NULL,
 CONSTRAINT [PK_GuiaAbateEquipamento] PRIMARY KEY CLUSTERED 
(
    [IDGuiaAbateEquipamento] ASC
)) ON [PRIMARY]

As you can see, nothing too fancy.

Now, since I can't reuse the default conventions, I've tried the following mappings for the GuiaAbate type:

public sealed class GuiaAbateTypeConfiguration: IEntityTypeConfiguration<GuiaAbate> 
{
    public void Configure(EntityTypeBuilder<GuiaAbate> builder) 
    {
        builder.ToTable("GuiaAbate");

        builder.HasKey(g => g.Id);
        builder.Property(g => g.Id).HasColumnName("IdGuiaAbate");

        builder.Property(g => g.Data)
               .HasConversion(d => d,
                              d => new DateTime(d.Ticks, DateTimeKind.Utc));

        builder.Property(g => g.Data)
               .HasConversion(
                   d => d,
                   d => new DateTime(d.Ticks, DateTimeKind.Utc));
        builder.Property(g => g.Tecnico).HasColumnName("Login");

        builder.HasMany(g => g.Equipamentos)
               .WithMany( )
               .UsingEntity(
                   "GuiaAbateEquipamento",
                   r => r.HasOne(typeof( Equipamento )).WithMany( ).HasForeignKey("IdEquipamento"),
                   l => l.HasOne(typeof( GuiaAbate )).WithMany( ).HasForeignKey("IdGuiaAbate"));
    }
}

The idea is to specify the name of the join table and the name of the columns that should be used as foreign keys on that table. Unfortunately, I must be missing something here because I end up with an error that says that

The skip navigation 'Equipamento.GuiaTransporte' doesn't have a foreign key associated with it. Every skip navigation must have a configured foreign key.

Can anyone help?

Thanks.


Solution

  • After trying everything, I've noticed that the error was on the GuiaTransporte mapping and not on the GuiaAbate. Anyway, the issue was on the configure left vs right lambdas: swapping them solved the problem (btw, the snippet I've shown for `GuiaAbate´ does work without any issues).