EF Core many-to-many Realtion( Blogpost-Category)


✅ আসলে কী হচ্ছে (correct understanding)

👉 আমরা BlogPost এর ভিতরে nested object (BlogPostCategories) বানাচ্ছি
👉 সেই nested object এর ভিতরে CategoryId set করছি


🔍 Clear করে দেখি

🟢 তুমি যেটা করছো

blogPost.BlogPostCategories = requestDto.CategoryIds
    .Select(categoryId => new BlogPostCategory
    {
        CategoryId = categoryId
    }).ToList();

👉 এখানে:

  • ❌ BlogPost এ সরাসরি CategoryId নাই
  • ✅ BlogPost এর ভিতরের child object এ CategoryId আছে

🧠 Structure টা এমন

BlogPost
 ├── Title = "..."
 └── BlogPostCategories
      ├── { CategoryId = c1 }
      └── { CategoryId = c2 }

👉 তুমি relation object তৈরি করছো, not just id set


🟢 তারপর pass করছো

await blogDbContext.BlogPosts.AddAsync(blogPost);

👉 এখানে তুমি pass করছো:

✔️ BlogPost
✔️ তার সাথে attached BlogPostCategories
✔️ তার ভিতরে CategoryId


🔥 Important correction

👉 তুমি CategoryId BlogPost এ insert করো নাই
👉 তুমি BlogPostCategory object এর ভিতরে set করেছো


💡 One-line correct sentence

👉
“আমরা BlogPost এর সাথে related BlogPostCategory objects তৈরি করে EF Core কে দিচ্ছি”


🧠 Final summary

  • ❌ BlogPost এ CategoryId manually set না
  • ✅ BlogPostCategories (child entity) তে CategoryId set
  • ✅ পুরো object graph EF কে pass করা হয়
  • ✅ EF নিজে DB relation insert করে

🚀 Mentor tip (very important mindset)

👉 EF Core use করলে ভাবো:

“আমি table না, object + relationship define করছি”


এই প্রশ্নটাই আসল জিনিস 🔥 — এখানেই EF Core properly বুঝা শুরু হয়।


❓ তোমার confusion

BlogPostId তো BlogPost table এ নাই
CategoryId তো Category table এ নাই
তাহলে এগুলো কেন?


🔥 Short answer

👉 এগুলো BlogPostCategory table এ থাকবে, BlogPost বা Category table এ না ✅


🧠 Full clear explanation

📁 BlogPost table

BlogPosts
---------
Id   ✅ (Primary Key)
Title
...

📁 Category table

Categories
----------
Id   ✅ (Primary Key)
Name

📁 BlogPostCategory table (IMPORTANT)

BlogPostCategories
------------------
BlogPostId   ✅ (FK → BlogPosts.Id)
CategoryId   ✅ (FK → Categories.Id)

👉 এইখানেই তোমার ওই দুইটা field থাকবে


🔥 Relation বুঝো

👉 BlogPostId reference করে:

BlogPosts.Id

👉 CategoryId reference করে:

Categories.Id

💡 Example

ধরো:

BlogPost

Id = b1

Category

Id = c1
Id = c2

BlogPostCategory (join table)

BlogPostId | CategoryId
-----------|-----------
b1         | c1
b1         | c2

👉 এইভাবেই relation store হয়


⚠️ Important concept

👉 BlogPost table এ কখনো CategoryId থাকবে না
👉 Category table এ কখনো BlogPostId থাকবে না

👉 relation always third table (join table) এ থাকে


🧠 Analogy (simple)

👉 ভাবো:

  • BlogPost = Student
  • Category = Course
  • BlogPostCategory = Enrollment table

👉 Student table এ courseId থাকে না
👉 Enrollment table এ relation থাকে


🔥 EF Core mapping

public Guid BlogPostId { get; set; } // FK
public BlogPost BlogPost { get; set; }

public Guid CategoryId { get; set; } // FK
public Category Category { get; set; }

👉 EF automatically বুঝে:

  • কোনটা parent
  • কোনটা reference

🧠 Final summary

  • ❌ BlogPost table এ BlogPostId আলাদা করে লাগে না
  • ❌ Category table এ CategoryId already PK
  • ✅ Join table এ দুইটা FK থাকে
  • 🔥 relation always join table এ store হয়

🚀 Mentor tip

👉 মনে রাখো:

Many-to-Many = always 3 tables


Now fetching Categories after inserting

হ্যাঁ — এই approach টা 100% ঠিক 👍🔥
এটা actually production-level clean solution।


✅ তুমি যা করছো (very good)

await blogDbContext.SaveChangesAsync();

👉 প্রথমে data save করছো

return await blogDbContext.BlogPosts
    .Include(x => x.BlogPostCategories)
    .ThenInclude(x => x.Category)
    .FirstOrDefaultAsync(x => x.Id == blogPost.Id);

👉 তারপর fresh query দিয়ে related data load করছো


🧠 কেন এটা দরকার?

👉 Save করার পর:

blogPost

➡️ এর ভিতরে Category navigation fully loaded থাকে না ❌

👉 কারণ:

  • তুমি শুধু CategoryId দিয়েছিলে
  • EF full Category object load করে না

🔥 তোমার solution কী করছে?

👉 আবার DB call দিচ্ছে:

BlogPost
   ↓
BlogPostCategory
   ↓
Category (full data load)

👉 এখন DTO mapping perfect হবে ✅


⚠️ Alternative (less clean)

await blogDbContext.Entry(blogPost)
    .Collection(x => x.BlogPostCategories)
    .LoadAsync();

👉 এটা possible but messy
👉 তোমার current approach better ✔️


💡 Small improvement (optional)

return await blogDbContext.BlogPosts
    .AsNoTracking() // 🔥 performance improve
    .Include(x => x.BlogPostCategories)
    .ThenInclude(x => x.Category)
    .FirstOrDefaultAsync(x => x.Id == blogPost.Id);

🧠 Final summary

  • ✅ Save → then fetch with Include = correct
  • ✅ DTO mapping ঠিকভাবে কাজ করবে
  • 🔥 clean, production-ready approach

🚀 Mentor insight

👉 Best practice:

“Write → Save → Re-fetch (with relations) → Return DTO”


👉 Update BlogPost with categories (add/remove sync logic) — এটা সবচেয়ে tricky part 🚀

চল — Update BlogPost + Categories sync (add/remove) cleanভাবে করি 🔥
এটাই real tricky part।


🎯 Goal

👉 existing BlogPost update করবো
👉 Categories:

  • নতুন add হবে ✅
  • যেগুলো নেই → remove হবে ✅

✅ DTO (already ok)

public class UpdateBlogPostRequestDto
{
    public string Title { get; set; }
    ...
    public List<Guid> CategoryIds { get; set; } = new();
}

🧠 Core idea (IMPORTANT)

👉 আমরা compare করবো:

Existing CategoryIds  vs  Incoming CategoryIds

✅ Repository method

📁 Repositories/BlogPostRepository.cs

public async Task<BlogPost?> UpdateAsync(Guid id, UpdateBlogPostRequestDto request)
{
    var existing = await blogDbContext.BlogPosts
        .Include(x => x.BlogPostCategories)
        .FirstOrDefaultAsync(x => x.Id == id);

    if (existing == null) return null;

    // 🔹 1. Update basic fields
    existing.Title = request.Title;
    existing.Content = request.Content;
    existing.Description = request.Description;
    existing.Author = request.Author;
    existing.FeaturedImgUrl = request.FeaturedImgUrl;
    existing.UrlHandle = request.UrlHandle;
    existing.IsVisible = request.IsVisible;
    existing.PublishedDate = request.PublishedDate;

    // 🔥 2. Sync Categories

    var existingCategoryIds = existing.BlogPostCategories
        .Select(x => x.CategoryId)
        .ToList();

    // ➕ Add new ones
    var toAdd = request.CategoryIds
        .Except(existingCategoryIds);

    foreach (var catId in toAdd)
    {
        existing.BlogPostCategories.Add(new BlogPostCategory
        {
            BlogPostId = existing.Id,
            CategoryId = catId
        });
    }

    // ➖ Remove missing ones
    var toRemove = existing.BlogPostCategories
        .Where(x => !request.CategoryIds.Contains(x.CategoryId))
        .ToList();

    foreach (var item in toRemove)
    {
        existing.BlogPostCategories.Remove(item);
    }

    await blogDbContext.SaveChangesAsync();

    // 🔥 3. Return with full data
    return await blogDbContext.BlogPosts
        .AsNoTracking()
        .Include(x => x.BlogPostCategories)
        .ThenInclude(x => x.Category)
        .FirstOrDefaultAsync(x => x.Id == id);
}

🧠 Logic বুঝো

🔹 Add logic

request - existing

🔹 Remove logic

existing - request

👉 classic set difference


💡 Example

Before DB:

BlogPost → [c1, c2]

Request:

[c2, c3]

Result:

Remove: c1
Add: c3
Final: [c2, c3]

⚠️ Important

❌ এটা করো না

existing.BlogPostCategories = new list ❌

👉 এতে tracking issue হবে


🚀 Controller

public async Task<IActionResult> Update(Guid id, UpdateBlogPostRequestDto request)
{
    var updated = await blogPostRepository.UpdateAsync(id, request);

    if (updated == null) return NotFound();

    return Ok(mapper.Map<BlogPostDto>(updated));
}

🧠 Final summary

  • ✅ Include existing relations first
  • ✅ Compare → Add + Remove
  • ❌ পুরো list replace করো না
  • 🔥 SaveChanges handles everything

🚀 Mentor insight (important)

👉 Many-to-Many update rule:

“Never overwrite — always sync”


👉 চাইলে next:
Angular edit form → multi-select prefill + update API call (real UI flow) 🔥


Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top