tandrewnichols
tandrewnichols

Reputation: 3466

Is there a way to model an optional many-to-many relationship in Entity Framework?

Is there a way in Entity Framework (and I assume it will be with fluent syntax as data annotations are somewhat limited) to model a many-to-many relationship in which both sides are optional (a 0..M to 0..N relationship)? The use case is this: I would like to allow users to add tags to entities. Tags to entities is a M:N relationship, but neither should be required. That is, a tag can exist that is not applied to any entities and an entity can be untagged. This seems fairly reasonable to me. I can't simply model this using:

public virtual ICollection<Tag> Tags { get; set; }

and

public virtual ICollection<Entity> Entities { get; set; }

because each class has other relationships, and I get a "foreign key constraint may cause cycles or multiple cascade paths." I was hoping maybe I could do something like:

modelBuilder.Entity<Tag>().HasOptional(t => t.Entities);
modelBuilder.Entity<Entity>().HasOptional(t => t.Tags);

but I am warned that EF is "Unable to determine the principal end of the association." From reading, it seems that such relationships HAVE to have a principal end, but in my case, that's undesirable.

I could add a class to represent the bridge table and handle the mapping manually, but I'd prefer not to clutter the code. I was wondering if there is another way to model this in EF.

To fill in a bit more detail, there is also an Author class (which amounts to Users). Authors and tags are 1:M and Authors to Entities are also 1:M. So of course, the problem is that the Entities class occurs twice in the cascade tree. Making the Tag/Entity relationship optional would fix this. I could also fix it if there was a way to get to Tags through Entities, but since Tags can exist without being connected to an entity, I figured that would be impossible.

Here's a summary of the related code:

public class Author
{
    public Guid Id { get; set; }
    public virtual List<Entity> Entities { get; set; }
    public virtual List<Tag> Tags { get; set; }
}

public class Tag
{
    public Guid Id { get; set; }
    public Guid AuthorId { get; set; }
    public virtual Author Author { get; set; }
    public virtual ICollection<Entity> Entities { get; set; }
}

public class Entity
{
    public Guid Id { get; set; }
    public Guid AuthorId { get; set; }
    public virtual Author Author { get; set; }
    public virtual ICollection<Tag> Tags { get; set; }
}

EDIT:

Using .HasMany().WithMany() as suggested below gives me this:

CREATE TABLE [dbo].[TagEntities] (
    [Tag_Id] [uniqueidentifier] NOT NULL,
    [Entity_Id] [uniqueidentifier] NOT NULL,
    CONSTRAINT [PK_dbo.TagEntities] PRIMARY KEY ([Tag_Id], [Entity_Id])
)

but what I WANT is for Tag_Id and Entity_Id to be nullable on this table. Maybe this model doesn't make as much sense as I thought?? Can you have a bridge table where both sides are nullable?

Upvotes: 4

Views: 2968

Answers (2)

tandrewnichols
tandrewnichols

Reputation: 3466

I don't know if this is the RIGHT answer, but I solved this by creating a base class called DbEntity that other classes inherited from. So now Author has just:

// Both entities and tags are part of this collection
public virtual List<DbEntity> Entities { get; set; }

Both "Entities" (which has special meaning in my code) and "Tags" subclass DbEntity. This eliminated the multiple cascade paths while preserving the navigation properties, although I do need to do this:

author.Entities.OfType<Tag>();

or

author.Entities.OfType<Entity>();

to get specific sets of entities.

Upvotes: 0

Eranga
Eranga

Reputation: 32437

Use

modelBuilder.Entity<Tag>().HasMany(t => t.Entities)
    .WithMany(t => t.Tags);

Instead of

modelBuilder.Entity<Tag>().HasOptional(t => t.Entities);
modelBuilder.Entity<Entity>().HasOptional(t => t.Tags);

Upvotes: 3

Related Questions