📁 Controller
[HttpPost]
public async Task<IActionResult> CreateBlogPost(CreateBlogPostRequestDto requestDto)
{
// 🔥 DTO → Domain
var blogPost = mapper.Map<BlogPost>(requestDto);
var created = await blogPostRepository.CreateAsync(blogPost, requestDto.CategoryIds);
return Ok(mapper.Map<BlogPostDto>(created));
}
📁 Repository
public async Task<BlogPost> CreateAsync(BlogPost blogPost, List<Guid> categoryIds)
{
blogPost.Id = Guid.NewGuid();
var ids = categoryIds?.Distinct().ToList() ?? new();
blogPost.BlogPostCategories = ids
.Select(id => new BlogPostCategory
{
BlogPostId = blogPost.Id,
CategoryId = id
}).ToList();
await blogDbContext.BlogPosts.AddAsync(blogPost);
await blogDbContext.SaveChangesAsync();
return await blogDbContext.BlogPosts
.AsNoTracking()
.Include(x => x.BlogPostCategories)
.ThenInclude(x => x.Category)
.FirstAsync(x => x.Id == blogPost.Id);
}
আগের Update লজিক আর এই Create লজিকের মধ্যে মূল পার্থক্য হলো—আপডেটের সময় আমাদের চেক করতে হতো কোনটা বাদ দেব আর কোনটা যোগ করব। কিন্তু Create করার সময় আমাদের হাতে কোনো পুরানো ডেটা নেই, সবকিছুই নতুন। তাই এখানে লজিকটা অনেক বেশি সহজ।
নিচে একদম ভেঙে বুঝিয়ে দিচ্ছি কী হচ্ছে:
১. কন্ট্রোলার (Controller) সেকশন: গেটওয়ে
এখানে ইউজার থেকে আসা DTO (কাঁচা ডেটা) কে Domain Model (ডেটাবেজ টেবিলের ফরম্যাট) এ রূপান্তর করা হয়।
C#
var blogPost = mapper.Map<BlogPost>(requestDto);
- কাজ: ইউজার যে টাইটেল, ডেসক্রিপশন পাঠিয়েছে, সেগুলোকে
BlogPostঅবজেক্টে কনভার্ট করা হয়। - এরপর এই অবজেক্ট এবং ক্যাটাগরি আইডিগুলোর লিস্ট (
requestDto.CategoryIds) রিপোজিটরিতে পাঠিয়ে দেওয়া হয়।
২. রিপোজিটরি (Repository) সেকশন: ডেটাবেজ ম্যাজিক
এখানেই আসল কাজটা হয়। তোমার ব্লগের সাধারণ ডেটার সাথে ক্যাটাগরিগুলোকে লিঙ্ক করা হয়।
ক) আইডি জেনারেট করা
C#
blogPost.Id = Guid.NewGuid();
যেহেতু এটি নতুন ব্লগ, তাই প্রথমেই এর জন্য একটি ইউনিক আইডি তৈরি করে নেওয়া হয়।
খ) ক্যাটাগরি ম্যাপিং (Select এর কারিশমা)
ইউজার পাঠিয়েছে আইডি লিস্ট (যেমন: [ID_1, ID_2]), কিন্তু আমাদের ডেটাবেজে BlogPostCategory টেবিলটি হলো একটি Join Table। তাই আমাদের আইডিগুলোকে অবজেক্টে রূপান্তর করতে হবে।
C#
blogPost.BlogPostCategories = ids.Select(id => new BlogPostCategory
{
BlogPostId = blogPost.Id, // ঐ যে একটু আগে বানানো ব্লগের আইডি
CategoryId = id // লুপ থেকে আসা ক্যাটাগরি আইডি
}).ToList();
- সহজ কথায়: এখানে
Selectএকটি লুপের মতো কাজ করছে। সে প্রতিটি ক্যাটাগরি আইডির জন্য একটি করেBlogPostCategoryরো (row) তৈরি করছে। - উদাহরণ: যদি ইউজার ২টা আইডি দেয়, তবে এই কোডটি ২টি নতুন রো তৈরি করে ব্লগের সাথে গেঁথে দেবে।
গ) সেভ করা
C#
await blogDbContext.BlogPosts.AddAsync(blogPost);
await blogDbContext.SaveChangesAsync();
এখানে এনটিটি ফ্রেমওয়ার্ককে বলা হচ্ছে—”এই ব্লগ এবং এর সাথে যুক্ত ক্যাটাগরিগুলো সব একসাথে ডেটাবেজে ঢুকিয়ে দাও।” সে এক কমান্ডেই দুই টেবিলে ডেটা পাঠিয়ে দেয়।
৩. রিটার্ন করা (পুরো ডেটাসহ)
সেভ করার পর আমরা সরাসরি blogPost রিটার্ন না করে আবার ডেটাবেজ থেকে কেন আনছি?
C#
return await blogDbContext.BlogPosts
.AsNoTracking()
.Include(x => x.BlogPostCategories)
.ThenInclude(x => x.Category) // ক্যাটাগরির নাম পাওয়ার জন্য
.FirstAsync(x => x.Id == blogPost.Id);
- কারণ: সেভ করার সময় আমাদের কাছে শুধু ক্যাটাগরি আইডি ছিল। কিন্তু ফ্রন্টএন্ডে বা ইউজারকে আমরা ক্যাটাগরির নাম (Name) দেখাতে চাই।
- এই কোডটি ডেটাবেজ থেকে ক্যাটাগরির নামসহ পুরো আপ-টু-ডেট ব্লগটি তুলে নিয়ে আসে।
সংক্ষেপে সারমর্ম (Comparison):
- Update-এ: আমরা লুপ চালিয়েছিলাম
Exceptদিয়ে (কী বাদ দেব আর কী যোগ করব)। - Create-এ: আমরা শুধু
Selectদিয়ে ইউজারের পাঠানো সব আইডিকে সরাসরি ব্লগের সাথে যুক্ত করে দিচ্ছি।
সহজ উদাহরণ:
তুমি একটা নতুন ফাইল ফোল্ডার বানাচ্ছ। ফোল্ডারটি বানানোর সময় তুমি তাতে কিছু নতুন কাগজ (Categories) ঢুকিয়ে দিলে। যেহেতু আগে কোনো কাগজ ছিল না, তাই তোমাকে কিছু মুছতে হলো না, শুধু ঢোকালই হয়ে গেল। এটাই হলো CreateAsync।
এই Select লজিকটা বুঝতে কি কোনো সমস্যা আছে? নাকি লুপের (foreach) বদলে কেন Select ব্যবহার হলো সেটা জানতে চাও?