0

My problem is in the title. I want to make a composite primary key as bidirectional. What I mean by bidrectional? Let me explain:

I have a friendship table. Primary keys are: SenderId and ReceiverId

In OnModelCreating:

builder.Entity<Friendship>()
                .HasKey(i => new { i.SenderId, i.ReceiverId });

So my friendship entities have primary keys like {SenderId, ReceiverId}. But not like {ReceiverId, SenderId}. I want to make keys to be both ways. In this way, my friendship requests will be unique and I will not create duplicate friendship requests as follows:

Friendship Table

Is this possible?

NOTE: I know how to check if there is a entry with those IDs.But I want to implement the database to reject the new entry with the same IDs in both ways.

Magnetron
  • 7,495
  • 1
  • 25
  • 41
Huntow
  • 11
  • In your case you just don't need to repeate the keys the second time. One pair will be plenty – Serge Jun 15 '21 at 16:30
  • @Serge This is what OP wants to prevent at database level, i.e. to **not allow** records with let say (1, 2) and (2, 1) keys – Ivan Stoev Jun 16 '21 at 06:53

2 Answers2

2

You need a key that conforms to a canonical form, i.e. always appear in the same order.

For example, instead of having a ReceiverID and a SenderID, you'd have two neutrally named fields (e.g. ParticipantA and ParticipantB) plus a constraint that ParticipantA must be less than ParticipantB. This way, you can set a constraint so that each pairing of IDs can only appear once, regardless of who is sending and who is receiving.

You'd then need to add a column that specifies direction of the relationship, e.g. who is the sender or if it is bidirectional.

Thus instead of

SenderID   ReceiverID
--------   ----------
1111       2222
4444       3333
1234       5678
5678       1234

You'd have

ParticipantA   ParticipantB  Sender
------------   ------------  ---------
1111           2222          A
3333           4444          B
1234           5678          BIDIRECTIONAL
Charlieface
  • 52,284
  • 6
  • 19
  • 43
John Wu
  • 50,556
  • 8
  • 44
  • 80
  • Thank you for your answer. I would like to have a database control that throws a error while entering duplicate entry, instead of having a new column that controls if it is duplicate or not. – Huntow Jun 15 '21 at 17:18
  • You misunderstand. There is no column that controls if it's a duplicate or not. There is a constraint that prevents duplicates. Because duplicates are no longer allowed, you need a column that tells you directionality. – John Wu Jun 15 '21 at 17:26
  • Oh okay, could you tell me how can I write that constraint with fluent API? @JohnWu – Huntow Jun 15 '21 at 18:01
  • 1
    @Huntow [Apparently not](https://stackoverflow.com/questions/34245449/is-it-possible-to-add-check-constraint-with-fluent-api-in-ef7) in pure fluent, you need to code the SQL `ALTER TABLE Friendship ADD CONSTRAINT Friendship_Id_Order CHECK (ParticipantA < ParticipantB)` – Charlieface Jun 15 '21 at 21:23
2

As @John Wu points out, you need a composite key where the containing key values are always in order, so (1, 2) and (2, 1) are both mapped to (1, 2), thus treated as equal.

However the proposed implementation requires too much changes to the database model and how you work with it. Also prevents having natural User.Senders and User.Receivers collection navigation properties if needed to obtain such information for a User.

So instead of modifying the existing primary data model, what you need is unique constraint (index) on a "normalized" composite key (User1Id, User2Id), where (in preudo code) User1Id = Min(SenderId, ReceiverId) and User2Id = Max(SenderId, ReceiverId).

The implementation of this is database specific. For SqlServer it can be implemented by creating two computed columns and then create unique constraint (index) on them, e.g.


// the two shadow properties mapped to the computed columns
modelBuilder.Entity<Friendship>()
    .Property<Guid>("UserId1")
    .HasComputedColumnSql("case when SenderId < ReceiverId then SenderId else ReceiverId end");

modelBuilder.Entity<Friendship>()
    .Property<Guid>("UserId2")
    .HasComputedColumnSql("case when SenderId < ReceiverId then ReceiverId else SenderId end");

// the unique index using them
modelBuilder.Entity<Friendship>()
    .HasIndex("UserId1", "UserId2")
    .IsUnique();

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 1
    Nice approach! Just one addition: I would make the computed columns persisted (stored = true). – Gert Arnold Jun 16 '21 at 07:52
  • Hi @Gert, wouldn't it be redundant since the computed values have to be stored in the index anyway? I'm not sure, just asking. – Ivan Stoev Jun 16 '21 at 08:00
  • Depends a bit on how the columns are going to be used. If it's *just* for the unique index it may be redundant. Actually, it's a broad subject, but I like to persist computed columns to give Sql Server the chance to skip re-evaluation, which, nevertheless, it won't always do. – Gert Arnold Jun 16 '21 at 10:48