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

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

📝 Lecture Summary (Quick Revision)

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

  • Initial Setup: GetAllPersons() কল করে ডাটাবেসের (লিস্টের) সব ডেটা নিয়ে আসা এবং matchingPersons নামের একটি ভ্যারিয়েবলে রাখা।
  • Input Null Check: searchBy বা searchString কোনোটি null বা খালি হলে সরাসরি সব Person-এর লিস্ট (allPersons) রিটার্ন করা।
  • Switch Case Logic: searchBy প্যারামিটারের ওপর ভিত্তি করে switch স্টেটমেন্ট ব্যবহার করা (যেমন: nameof(PersonResponse.PersonName) এর জন্য একটি কেস)।
  • LINQ Filtering: প্রতিটি case-এর ভেতরে LINQ-এর Where মেথড ব্যবহার করে ফিল্টারিং করা।
  • Case-Insensitive Search: স্ট্রিং সার্চ করার সময় Contains() মেথডের ভেতরে StringComparison.OrdinalIgnoreCase ব্যবহার করা।
  • Handling Date Types: DateOfBirth (DateTime) এর মতো নন-স্ট্রিং ডেটার ক্ষেত্রে আগে .ToString() এ কনভার্ট করে তারপর Contains() দিয়ে সার্চ করা।
  • Null Reference Prevention: যেকোনো প্রপার্টিতে Contains() কল করার আগে সেটি null কিনা তা চেক করে নেওয়া, যেন রানটাইমে NullReferenceException না আসে।

🧠 Comprehensive Breakdown

এই লেকচারে আমরা একটি ডায়নামিক সার্চ মেকানিজম তৈরি করেছি। চলুন প্রতিটি ধাপ বিস্তারিতভাবে বুঝে নিই।

১. Initial Setup এবং Input Validation (Priority: 9/10)

Concept & Why: যখন ইউজার কোনো কিছু সার্চ করে, তখন আমাদের প্রথমে সব ডেটা হাতে পেতে হবে। তাই আমরা GetAllPersons() কল করেছি। এরপর চেক করেছি যে, ইউজার আদৌ কোনো সার্চ প্যারামিটার দিয়েছে কিনা। যদি searchBy বা searchString ফাঁকা থাকে, তবে সার্চ করার কিছু নেই; তাই আমরা সম্পূর্ণ লিস্ট রিটার্ন করে দেব।

public List<PersonResponse> GetFilteredPersons(string searchBy, string? searchString)
{
    // ১. প্রথমে সব Person নিয়ে আসা
    List<PersonResponse> allPersons = GetAllPersons();
    List<PersonResponse> matchingPersons = allPersons;
 
    // ২. ইনপুট চেক করা
    if (string.IsNullOrEmpty(searchBy) || string.IsNullOrEmpty(searchString))
    {
        return matchingPersons;
    }
    
    // ... পরবর্তী লজিক
}
 

২. Switch Case দিয়ে Property সিলেকশন (Priority: 10/10)

Concept & Why: ইউজার কোন ফিল্ড (Name, Email, Address) ধরে সার্চ করতে চাইছে, তা আইডেন্টিফাই করার জন্য switch স্টেটমেন্ট ব্যবহার করা হয়েছে। এখানে হার্ডকোডেড স্ট্রিং (যেমন "PersonName") ব্যবহার না করে nameof অপারেটর ব্যবহার করা হয়েছে, যা টাইপো (Typo) রোধ করে।

switch (searchBy)
{
    case nameof(PersonResponse.PersonName):
        // Name দিয়ে সার্চ করার লজিক
        break;
 
    case nameof(PersonResponse.Email):
        // Email দিয়ে সার্চ করার লজিক
        break;
        
    // ... অন্যান্য প্রপার্টি
}
 

৩. LINQ-এর মাধ্যমে Filtering এবং Case-Insensitive Search (Priority: 10/10)

Concept & Why: switch-এর ভেতরে আমরা LINQ-এর Where ক্লজ ব্যবহার করে ফিল্টার করেছি।

  • Null Check: temp.PersonName যদি null হয় এবং আমরা তার ওপর .Contains() কল করি, তবে NullReferenceException খেয়ে অ্যাপ্লিকেশন ক্র্যাশ করবে। তাই আগে string.IsNullOrEmpty() দিয়ে চেক করা হয়েছে।
  • (নোট: লেকচারার বলেছেন প্রপার্টি null হলে true রিটার্ন করতে (blindly take that record), কিন্তু সাধারণত সার্চের ক্ষেত্রে null থাকলে false রিটার্ন করাটা স্ট্যান্ডার্ড, যাতে অপ্রাসঙ্গিক ডেটা না আসে)।
  • Case Insensitivity: Contains(searchString, StringComparison.OrdinalIgnoreCase) ব্যবহার করা হয়েছে, যাতে ইউজার ছোট বা বড় হাতের অক্ষর যাই লিখুক না কেন, ডেটা ঠিকমতো ম্যাচ করে।
case nameof(PersonResponse.PersonName):
    matchingPersons = allPersons.Where(temp =>
        // null চেক করা হচ্ছে
        (!string.IsNullOrEmpty(temp.PersonName) ? 
        // Case-Insensitive সার্চ
        temp.PersonName.Contains(searchString, StringComparison.OrdinalIgnoreCase) : true)
    ).ToList();
    break;
 

৪. DateOfBirth (DateTime) কনভার্সন (Priority: 8/10)

Concept & Why: Contains() মেথডটি শুধুমাত্র string-এর ওপর কাজ করে। কিন্তু DateOfBirth হলো DateTime? (Nullable DateTime) টাইপ। তাই একে সার্চ করতে হলে প্রথমে null চেক করে, তারপর .ToString() দিয়ে স্ট্রিংয়ে কনভার্ট করতে হবে।

case nameof(PersonResponse.DateOfBirth):
    matchingPersons = allPersons.Where(temp =>
        (temp.DateOfBirth != null) ? 
        temp.DateOfBirth.Value.ToString("dd MMM yyyy").Contains(searchString, StringComparison.OrdinalIgnoreCase) : true
    ).ToList();
    break;
 

৫. Unit Test Results (Priority: 6/10)

লজিক লেখা শেষ করে Test Explorer রান করলে দেখা যায়, আগের লেকচারে লেখা GetFilteredPersons_EmptySearchText এবং GetFilteredPersons_SearchByPersonName দুটো টেস্টই সফলভাবে পাস করেছে। এবং ITestOutputHelper থাকার কারণে আমরা Console/Details উইন্ডোতে Expected এবং Actual ডেটা দেখতে পাচ্ছি।


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

লেকচারের switch-case পদ্ধতিটি কাজ করলেও, এটি অনেক লম্বা এবং বয়লারপ্লেট (Boilerplate) কোডে ভর্তি। মডার্ন C# (C# 8 এবং তার পরের ভার্সনে) Switch Expressions ব্যবহার করে এই কোডটিকে অনেক বেশি রিডেবল এবং ক্লিন করা যায়। এছাড়া, লেকচারার একটু উল্লেখ করেছিলেন Reflection-এর কথা, যা কোড রিপিটেশন কমায়।

Modern Update (Using C# Switch Expression & Safer Null Checking):

public List<PersonResponse> GetFilteredPersons(string searchBy, string? searchString)
{
    List<PersonResponse> allPersons = GetAllPersons();
 
    if (string.IsNullOrWhiteSpace(searchBy) || string.IsNullOrWhiteSpace(searchString))
        return allPersons;
 
    // C# 8+ Switch Expression - অনেক বেশি ক্লিন!
    return searchBy switch
    {
        nameof(PersonResponse.PersonName) => allPersons.Where(p => 
            p.PersonName?.Contains(searchString, StringComparison.OrdinalIgnoreCase) ?? false).ToList(),
            
        nameof(PersonResponse.Email) => allPersons.Where(p => 
            p.Email?.Contains(searchString, StringComparison.OrdinalIgnoreCase) ?? false).ToList(),
            
        nameof(PersonResponse.DateOfBirth) => allPersons.Where(p => 
            p.DateOfBirth?.ToString("dd MMM yyyy").Contains(searchString, StringComparison.OrdinalIgnoreCase) ?? false).ToList(),
            
        _ => allPersons // Default case (যদি কোনোটি ম্যাচ না করে)
    };
}
 

Why this is smarter:

  • switch expression অনেক কম কোড ব্যবহার করে।
  • p.PersonName?.Contains(...) ?? false হলো Null-conditional (?.) এবং Null-coalescing (??) অপারেটরের দুর্দান্ত একটি ব্যবহার। যদি PersonName null হয়, তবে এটি ক্র্যাশ না করে ডানপাশের false রিটার্ন করবে। এর ফলে লেকচারের সেই অদ্ভুত if-else বা টার্নারি অপারেটরের প্রয়োজন হয় না।

🏆 Best Practices (For Searching and LINQ)

  1. Always Use OrdinalIgnoreCase: স্ট্রিং কম্পেয়ার বা সার্চ করার সময় ToLower() বা ToUpper() ব্যবহার করার চেয়ে StringComparison.OrdinalIgnoreCase ব্যবহার করা অনেক বেশি পারফরম্যান্স-বান্ধব এবং বেস্ট প্র্যাকটিস।
  2. Safe Null Handling in LINQ: LINQ-এর ভেতরে অবজেক্টের প্রপার্টি অ্যাক্সেস করার সময় সবসময় ?. (Null-conditional operator) ব্যবহার করবেন।
  3. Avoid Client-Side Evaluation (For Future): বর্তমানে আমরা GetAllPersons() দিয়ে ডাটাবেসের সব ডেটা মেমরিতে (RAM) এনে তারপর C# দিয়ে ফিল্টার করছি (Client-side evaluation)। এটি In-memory লিস্টের জন্য ঠিক আছে। কিন্তু ভবিষ্যতে যখন Entity Framework (EF Core) ব্যবহার করবেন, তখন ডাটাবেস থেকে সব ডেটা না এনে সরাসরি IQueryable এর ওপর ফিল্টার করে তারপর .ToList() করতে হবে, যাতে ফিল্টারিংয়ের কাজ ডাটাবেস সার্ভারে (SQL) হয়।