🧩 The Challenge: Managing Tags Without Duplicates
#

In many .NET applications, entities often have many-to-many relationships. A common example is an entity associated with multiple tags. The challenge arises when you want to:

  • Prevent duplicate tags in the database.
  • Maintain clean and maintainable code without manually handling join entities.

Traditionally, this would involve creating an explicit join entity (e.g., EntityTag) and managing it directly, which can become cumbersome.


🚀 The Solution: Skip Navigation Properties in EF Core
#

Starting with EF Core 5.0, you can leverage Skip Navigation Properties to handle many-to-many relationships without explicitly defining the join entity in your code. EF Core manages the join table behind the scenes, allowing you to focus on the primary entities.

✅ Benefits:
#

  • Simplified codebase: No need to define and manage join entities explicitly.
  • Cleaner entity models: Direct navigation properties between related entities.
  • Maintainability: Easier to read and maintain relationships.

🛠️ Implementing Skip Navigation Properties
#

Let’s consider an example where an Article can have multiple Tags, and each Tag can be associated with multiple Articles.

1. Define the Entities:
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ArticleEntity
{
    public Guid Id { get; set; }
    public string Title { get; set; }

    public virtual ICollection<TagEntity> Tags { get; set; } = [];
}

public class TagEntity
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public ICollection<ArticleEntity> Articles { get; set; } = [];
}

public class ArticleTagEntity
{
    public Guid ArticleId { get; set; }
    public Guid TagId { get; set; }

    public ArticleEntity Article { get; set; }
    public TagEntity Tag { get; set; }
}

2. Configure the Relationship Using Fluent API:
#

Even though EF Core can configure many-to-many relationships by convention, it’s good practice to define them explicitly, especially if you need to customize the join table.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ArticleEntity>()
        .HasMany(a => a.Tags)
        .WithMany(t => t.Articles)
        .UsingEntity<ArticleTagEntity>(
            "ArticleTag",
            e => e.HasOne<Tag>().WithMany().HasForeignKey("TagId"),
            e => e.HasOne<Article>().WithMany().HasForeignKey("ArticleId"));
}

In this configuration:

  • "ArticleTag" is the name of the join table.
  • Foreign keys are explicitly defined for clarity.

🔄 Managing Tags Without Duplicates
#

To ensure that tags are not duplicated in the database:

  1. Check if the tag exists before adding it to an article.
  2. Reuse existing tags instead of creating new ones with the same name.

Here’s how you might implement this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public async Task AddTagToArticleAsync(int articleId, string tagName)
{
    var article = await _context.Articles
        .Include(a => a.Tags)
        .FirstOrDefaultAsync(a => a.Id == articleId);

    if (article == null) 
    {
      return;
    }
    var tag = await _context.Tags
        .FirstOrDefaultAsync(t => t.Name == tagName);
    if (tag == null)
    {
        tag = new Tag { Name = tagName };
        _context.Tags.Add(tag);
    }
    if (!article.Tags.Any(t => t.Id == tag.Id))
    {
        article.Tags.Add(tag);
        await _context.SaveChangesAsync();
    }
}

This method:

  • Retrieves the article and its associated tags.
  • Checks if the tag exists; if not, creates it.
  • Adds the tag to the article if it’s not already associated.

🧪 Considerations
#

  • Explicit Join Entity: If you need to store additional data in the join table (e.g., timestamps, metadata), you’ll need to define an explicit join entity.
  • Performance: Always consider the performance implications of loading related data, especially in large datasets. Use .Include() judiciously.
  • Data Integrity: Implement appropriate validation to maintain data integrity, especially when dealing with user-generated tags.

📚 Further Reading
#

For more detailed information, refer to the official EF Core documentation on many-to-many relationships.