Home > Software design >  Implementing two-directional navigation properties between models
Implementing two-directional navigation properties between models

Time:05-29

When trying to add a Razor Page using Entity Framework (CRUD) for my Player model, I get the follow exception

Unable to determine the relationship represented by navigation 'Player.Alliance' of type 'Alliance'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'
public class Player
{
    public Guid PlayerID { get; init; }

    // Navigation properties
    public virtual Alliance Alliance { get; init; }

    // Properties
    public int          GovernorIdentifier { get; set; }
    public string       Name               { get; set; }
    public int          Power              { get; set; }
    public int          Killpoints         { get; set; }
    public Civilization Civilization       { get; set; }
    public int          HighestPower       { get; set; }
    public int          Victory            { get; set; }
    public int          Defeat             { get; set; }
    public int          Dead               { get; set; }
    public int          ScoutTimes         { get; set; }
    public int          ResourcesGathered  { get; set; }
    public int          ResourceAssistance { get; set; }
    public int          AllianceHelpTimes  { get; set; }
}
public class Alliance
{
    // Primary key
    public Guid AllianceID { get; init; }

    // Navigation properties
    public Player              Leader  { get; init; }
    public ICollection<Player> Players { get; init; }

    // Properties
    public string Tag            { get; set; }
    public string Name           { get; set; }
    public int    Territories    { get; set; }
    public int    GiftLevel      { get; set; }
    public int    PlayerCapacity { get; set; }
}

When I remove public Player Leader { get; init; } from Alliance, or when I remove public virtual Alliance Alliance { get; set; } from Player, the error is resolved.

How do I implement a two-directional one-to-one (don't kill me if this is the wrong terminology) relation between Player and Alliance? In my specific use case, how do I make sure Alliance has both a list of players, and a navigation property to a player that is the alliance leader, and Player has a navigation property to the alliance it is a part off?

CodePudding user response:

This occurs when you rely on EF conventions for mapping the relationships. The problem is in your Alliance entity/table. You have:

public Player              Leader  { get; init; }
public ICollection<Player> Players { get; init; }

In your your Player, you will have an AllianceId which EF whould use to resolve the Players associated to an Alliance, but your Alliance table will also need a FK to resolve the Player record that is the leader.

EF can work out many-to-one keys by convention, but it does so based on the Type name, not the property name. In your Alliance table you likely have something like a LeaderId, or better, a LeaderPlayerId. EF won't work that out automatically so you need to explicitly map it.

For EF Core you can use a shadow property if you don't want to expose the FK as a property in Alliance. (I recommend not exposing FKs alongside navigation properties to avoid two sources of truth) Depending on what mapping method you are using, whether using OnModelCreating /w modelBuilder, or IEntityTypeConfiguration, or relying on convention you can set up a FK shadow proeprty:

Convention using Attributes:

[ForeignHey("LeaderPlayerId")]
public virtual Player Leader { get; set; }

With the modelBuilder or EntityTypeConfiguration you use the .HasForeignKey("LeaderPlayerId") with the .HasOne(...).WithMany() expression.

The limitation here is that by tracking the LeaderPlayerId on the Alliance, there is no way to enforce at a data layer that the Leader's AllianceId is actually that alliance. It is just an arbitrary FK back to any player.

We cannot use the AllianceId on the Player to establish something like a One-to-One relationship for the leader because that FK is used to determine which alliance that player belongs to.

The other alternative with the current structure if you don't want the LeaderPlayerId in the Alliance table, would be to use an "IsLeader" flag on the player. This would be used in combination with the Alliance ID to indicate that this player is a Leader of their associated Alliance. This would mean that if you had a Rule that Alliances could only have one leader then you would need to enforce that rule in code and use data health checks to detect if somehow an Alliance was assigned more than one leader somehow. Data health check scripts can be useful to set up to address the "leader isn't part of this alliance" situation with the LeaderPlayerId approach as well.

  • Related