আমি আপনার এক্সপার্ট সফটওয়্যার ইঞ্জিনিয়ারিং ট্রেইনার। কোর্স আউটলাইন অনুযায়ী, আমরা এখন Section 15: xUnit testing-এর ভেতরে আছি। আগের লেকচারে আমরা GetCountryByCountryId মেথডের জন্য Unit Test লিখেছিলাম, আর এই লেকচারে আমরা সেই মেথডটির আসল লজিক (Implementation) লিখব এবং টেস্ট রান করে দেখব।

চলুন, আজকের লেকচারটি সহজভাবে এবং বিস্তারিতভাবে বুঝে নেওয়া যাক।

📝 Lecture Summary (Quick Revision)

ভবিষ্যতে দ্রুত রিভিশন দেওয়ার জন্য সম্পূর্ণ লেকচারের মূল কাজগুলো নিচে লিস্ট আকারে দেওয়া হলো:

  • Null Check: মেথডের ইনপুট countryID যদি null হয়, তবে সরাসরি null রিটার্ন করা।
  • LINQ FirstOrDefault: _countries লিস্ট থেকে নির্দিষ্ট countryID অনুযায়ী প্রথম ম্যাচিং Country অবজেক্টটি খুঁজে বের করা।
  • Domain Model to DTO: খুঁজে পাওয়া Country (Domain Model) অবজেক্টটিকে ToCountryResponse() মেথডের মাধ্যমে CountryResponse (DTO) তে কনভার্ট করা।
  • Data Hiding: সরাসরি Domain Model রিটার্ন না করে DTO রিটার্ন করা, যাতে বাইরের Controller বা Unit Test ভেতরের ডাটাবেস স্ট্রাকচার জানতে না পারে।
  • Test Validation: আগের লেকচারে লেখা xUnit টেস্টগুলো রান করে চেক করা যে আমাদের লেখা লজিক ঠিকমতো কাজ করছে কিনা।
  • Failing Test Demo: ইচ্ছে করে ভুল কোড (return null;) লিখে দেখানো যে xUnit কীভাবে ভুল ধরিয়ে দেয় (Expected vs Actual)।

🧠 Comprehensive Breakdown

এই লেকচারে আমরা CountriesService ক্লাসের ভেতরে GetCountryByCountryId মেথডটি সম্পূর্ণ করেছি। চলুন এর প্রতিটি ধাপ ও কনসেপ্ট বিস্তারিতভাবে বুঝে নিই।

১. Parameter Null Checking (Priority: 8/10)

Concept & Why: যখন কোনো Controller বা ক্লায়েন্ট এই Service মেথডটি কল করবে, তারা ভুল করে countryID হিসেবে null পাঠাতে পারে। যদি আমরা null ভ্যালু দিয়ে ডাটাবেসে বা লিস্টে খুঁজতে যাই, তবে NullReferenceException আসতে পারে। তাই মেথডের শুরুতেই চেক করে নেওয়া হয় ইনপুট null কিনা। যদি null হয়, তবে সরাসরি null রিটার্ন করে দেওয়া হয়।

if (countryID == null)
{
    return null;
}
 

২. Searching with LINQ FirstOrDefault (Priority: 10/10)

Concept & Why: লিস্টের ভেতর থেকে নির্দিষ্ট কোনো শর্ত (Condition) অনুযায়ী ডেটা খুঁজে বের করার জন্য LINQ (Language Integrated Query) ব্যবহার করা হয়। এখানে আমরা FirstOrDefault() মেথড ব্যবহার করেছি।

How it works: FirstOrDefault একটি Lambda Expression গ্রহণ করে (যেমন: temp => temp.CountryID == countryID)। এটি লিস্টের প্রথম এলিমেন্ট থেকে শুরু করে লুপের মতো চেক করতে থাকে।

  • যদি প্রথম এলিমেন্টেই কন্ডিশন মিলে যায়, তবে সে আর সামনের দিকে না গিয়ে সাথে সাথে সেই অবজেক্টটি রিটার্ন করে দেয়।
  • যদি না মিলে, তবে পরেরটিতে যায়।
  • যদি পুরো লিস্ট খুঁজেও কোনো অবজেক্ট না পায় (অর্থাৎ ওই ID-এর কোনো Country নেই), তবে এটি Exception থ্রো না করে ডিফল্ট ভ্যালু (Reference Type এর ক্ষেত্রে null) রিটার্ন করে।
// লিস্ট থেকে matching ID এর Country খুঁজে বের করা
Country? country_response_from_list = _countries.FirstOrDefault(temp => temp.CountryID == countryID);
 

৩. Domain Model to DTO Conversion (Priority: 10/10)

Concept & Why: FirstOrDefault থেকে আমরা যা পাবো, সেটি হলো Country ক্লাসের অবজেক্ট (Domain Model)। কিন্তু আমাদের মেথডের রিটার্ন টাইপ হলো CountryResponse (DTO)।

Security Reason: লেকচারার স্পষ্টভাবে বলেছেন, “We should not expose the domain model classes to outside the service.” অর্থাৎ, আমাদের ডাটাবেসের আসল টেবিল স্ট্রাকচার (Domain Model) Controller বা বাইরের দুনিয়াকে দেখানো উচিত নয়। তাই আমরা Country কে CountryResponse এ কনভার্ট করে রিটার্ন করব।

যেহেতু country_response_from_list ভ্যালুটি null হতে পারে (যদি ওই ID তে কিছু না পাওয়া যায়), তাই আমরা সরাসরি কনভার্ট না করে চেক করে নেব যে অবজেক্টটি null কিনা।

৪. Complete Implementation (As per Transcript)

লেকচারারের দেখানো স্টেপগুলো মিলিয়ে সম্পূর্ণ কোডটি নিচে দেওয়া হলো:

public CountryResponse? GetCountryByCountryId(Guid? countryID)
{
    // ১. Null Check
    if (countryID == null)
    {
        return null;
    }
 
    // ২. Find matching country
    Country? country_response_from_list = _countries.FirstOrDefault(temp => temp.CountryID == countryID);
 
    // ৩. Check if found, then convert to DTO
    if (country_response_from_list != null)
    {
        return country_response_from_list.ToCountryResponse();
    }
 
    // ৪. If not found
    return null;
}
 

৫. Unit Test Validation & Failing Test Demo (Priority: 7/10)

Concept: কোড লেখা শেষ হলে Visual Studio-এর Test Explorer থেকে “Run All Tests” করা হয়। আমাদের লেখা লজিক সঠিক থাকায় আগের লেকচারের টেস্টগুলো (যেমন: GetCountryByCountryId_ValidCountryId এবং GetCountryByCountryId_NullCountryId) পাস করে।

(VS Code Tip: আপনি যদি VS Code ব্যবহার করেন, তবে টার্মিনালে dotnet test কমান্ড লিখে টেস্টগুলো রান করতে পারবেন।)

Demo: লেকচারার এরপর ইচ্ছাকৃতভাবে মেথডটির শেষে সঠিক ডেটা রিটার্ন না করে return null; লিখে টেস্ট রান করেন। তখন xUnit টেস্টটি ফেইল করে এবং Error Message দেখায়:

  • Expected: CountryResponse object
  • Actual: null এর মাধ্যমে প্রমাণিত হয় যে, TDD (Test Driven Development) অ্যাপ্রোচে টেস্ট কেসগুলো আমাদের ভুল কোড প্রোডাকশনে যাওয়া থেকে রক্ষা করে।

🚀 Smarter & Updated Approach (.NET 8/10 & Modern C#)

লেকচারে দেখানো কোডটি একেবারে বেসিক এবং সঠিক। তবে মডার্ন C# (C# 9 থেকে C# 12+) ব্যবহার করে এই কোডটিকে আরও অনেক বেশি ক্লিন, রিডেবল এবং ছোট (Concise) করা যায়।

Modern Code Implementation:

public CountryResponse? GetCountryByCountryId(Guid? countryID)
{
    // C# 9+ Pattern Matching (is null) is slightly faster and safer than (== null)
    if (countryID is null)
        return null;
 
    // Using Null-conditional operator (?.) simplifies the null check and conversion into one line
    return _countries.FirstOrDefault(temp => temp.CountryID == countryID)?.ToCountryResponse();
}
 

Why this is better:

  • is null ব্যবহার করলে এটি সরাসরি মেমরি রেফারেন্স চেক করে, যদি কেউ কোনো ক্লাসে == অপারেটর ওভাররাইড করেও রাখে তাও এটি ভুল করবে না।
  • ?. (Null-conditional operator) চেক করে যে FirstOrDefault থেকে আসা ডেটা null কিনা। যদি null হয়, তবে সে ডানপাশের ToCountryResponse() কল না করে সরাসরি null রিটার্ন করে। আর ডেটা থাকলে তবেই মেথডটি কল করে। এতে ৩-৪ লাইনের if-else ব্লক ১ লাইনে নেমে আসে।

🏆 Best Practices (Service Layer & LINQ)

  1. Use FirstOrDefault over Where().FirstOrDefault(): কখনোই _countries.Where(x => x.Id == id).FirstOrDefault() লিখবেন না। এটি ডাটাবেস বা মেমরিতে ডাবল কাজ করে। সরাসরি কন্ডিশনটি FirstOrDefault(x => x.Id == id) এর ভেতরে দিয়ে দেওয়া বেস্ট প্র্যাকটিস।
  2. Hide Domain Entities: Service লেয়ার থেকে সব সময় DTO (Data Transfer Object) রিটার্ন করবেন। ডাটাবেসের Entity (যেমন Country, Person) কখনোই Controller-এ পাঠাবেন না।
  3. Use Modern C# Features (.NET 10 Context): .NET 8/10 এবং C# 12+ ভার্সনে কোড যত ক্লিন রাখা যায় তত ভালো। Nullability নিয়ে কাজ করার সময় ?. (Null-conditional) এবং ?? (Null-coalescing) অপারেটরগুলোর সর্বোচ্চ ব্যবহার করা উচিত, যা কোডের রিডেবিলিটি বহুগুণ বাড়িয়ে দেয়।