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

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

📝 Lecture Summary (Quick Revision)

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

  • Validation Logic: PersonAddRequest এবং এর প্রপার্টি PersonName null কিনা চেক করা এবং এক্সেপশন থ্রো করা।
  • Data Store Initialization: PersonsService ক্লাসে একটি প্রাইভেট _persons লিস্ট তৈরি করা, যা সাময়িকভাবে ডাটাবেস হিসেবে কাজ করবে।
  • DTO to Domain Model: ToPerson() মেথড ব্যবহার করে ইনপুট DTO-কে Person মডেলে কনভার্ট করা এবং নতুন Guid জেনারেট করে লিস্টে ইনসার্ট করা।
  • Dependency Injection (DI): ICountriesService-কে PersonsService-এর কনস্ট্রাক্টরে ইনজেক্ট করা, যাতে Person-এর Country ID দিয়ে Country Name বের করা যায়।
  • Private Helper Method: ConvertPersonToPersonResponse() নামে একটি প্রাইভেট মেথড তৈরি করা, যা Person অবজেক্টকে PersonResponse-এ কনভার্ট করে এবং Country Name লোড করে।
  • Null Conditional Operator (?.): Country Name লোড করার সময় NullReferenceException ঠেকানোর জন্য ?. অপারেটরের ব্যবহার।
  • Testing Result: টেস্ট রান করার পর প্রথম দুটি ভ্যালিডেশন টেস্ট পাস করে, কিন্তু তৃতীয় টেস্টটি ফেইল করে, কারণ GetAllPersons মেথডটি এখনো ইমপ্লিমেন্ট করা হয়নি।

🧠 Comprehensive Breakdown

এই লেকচারে আমরা মূলত AddPerson মেথডের পূর্ণাঙ্গ জীবনচক্র (Life-cycle) ইমপ্লিমেন্ট করেছি। চলুন প্রতিটি ধাপের লজিক এবং পেছনের কারণ (Why) বুঝে নিই।

১. Null & Parameter Validations (Priority: 9/10)

Concept & Why: ডাটাবেসে বা লিস্টে ডেটা ইনসার্ট করার আগে নিশ্চিত করতে হবে যে ক্লায়েন্ট থেকে আসা ডেটাগুলো ভ্যালিড। যদি অবজেক্ট null হয়, তবে ArgumentNullException এবং যদি কোনো স্পেসিফিক ফিল্ড (যেমন PersonName) ফাঁকা থাকে, তবে ArgumentException থ্রো করা হয়।

public PersonResponse AddPerson(PersonAddRequest? personAddRequest)
{
    // ১. Request Object Null Check
    if (personAddRequest == null)
    {
        throw new ArgumentNullException(nameof(personAddRequest));
    }
 
    // ২. Property Check (Null or Empty)
    if (string.IsNullOrEmpty(personAddRequest.PersonName))
    {
        throw new ArgumentException("Person name cannot be blank");
    }
 
    // ... next steps
}
 

২. Data Store Initialization (Priority: 7/10)

Concept & Why: যেহেতু আমরা এখনো Entity Framework ব্যবহার করছি না, ডেটা স্টোর করার জন্য একটি In-memory List প্রয়োজন। PersonsService ক্লাসের শুরুতেই একটি List<Person> তৈরি করা হয়েছে।

private readonly List<Person> _persons;
 
// Constructor
public PersonsService()
{
    _persons = new List<Person>();
}
 

৩. DTO -> Model -> Insert -> Model -> DTO (Priority: 10/10)

Concept & Why:

  1. ক্লায়েন্ট পাঠাবে DTO (PersonAddRequest)।
  2. সেটিকে ডাটাবেস মডেলে (Person) কনভার্ট করতে হবে (.ToPerson())।
  3. একটি নতুন ID (Guid.NewGuid()) জেনারেট করে ডাটাবেসে (লিস্টে) সেভ করতে হবে।
  4. সবশেষে সেই মডেলকে আবার রেসপন্স DTO-তে (PersonResponse) কনভার্ট করে ক্লায়েন্টকে রিটার্ন করতে হবে।

(আমরা সরাসরি মডেল রিটার্ন করি না, কারণ এটি সিকিউরিটি রিস্ক তৈরি করে এবং ডাটাবেসের ভেতরের স্ট্রাকচার লিক হয়ে যায়)।

৪. Dependency Injection for Country Service (Priority: 10/10)

Concept & Why: আমাদের Person মডেলে শুধু CountryID আছে। কিন্তু PersonResponse DTO-তে আমরা Country (দেশের নাম) দেখাতে চাই। দেশের নাম বের করার জন্য আমাদের CountriesService-এর GetCountryByCountryId মেথডটি লাগবে। এজন্য ICountriesService-কে PersonsService-এর কনস্ট্রাক্টরে Inject করা হয়েছে। এটি আর্কিটেকচারের একটি দারুণ উদাহরণ, যেখানে দুটি সার্ভিস নিজেদের মধ্যে যোগাযোগ করছে।

private readonly List<Person> _persons;
private readonly ICountriesService _countriesService;
 
// Constructor Injection
public PersonsService()
{
    _persons = new List<Person>();
    _countriesService = new CountriesService(); // আপাতত সরাসরি অবজেক্ট, পরে DI কন্টেইনার আসবে
}
 

৫. Private Helper Method for Reusability (Priority: 9/10)

Concept & Why: Person-কে Response-এ কনভার্ট করে তার সাথে Country Name জুড়ে দেওয়ার কাজটি শুধু AddPerson-এ নয়, ভবিষ্যতে GetAllPersons বা UpdatePerson মেথডেও লাগবে। একই কোড বারবার (Repeat) না লেখার জন্য একটি private মেথড তৈরি করা হয়েছে। একে বলা হয় DRY (Don’t Repeat Yourself) প্রিন্সিপাল।

এখানে লেকচারার Null Conditional Operator (?.) ব্যবহার করেছেন। যদি GetCountryByCountryId কোনো Country খুঁজে না পায় (অর্থাৎ null রিটার্ন করে), তবে .CountryName কল করলে সিস্টেম ক্র্যাশ করত। ?. দেওয়ার কারণে সিস্টেম ক্র্যাশ না করে সরাসরি null রিটার্ন করবে।

// Helper Method
private PersonResponse ConvertPersonToPersonResponse(Person person)
{
    PersonResponse personResponse = person.ToPersonResponse();
    
    // Country ID দিয়ে Country Name বের করা এবং null conditional operator ব্যবহার
    personResponse.Country = _countriesService.GetCountryByCountryId(person.CountryID)?.CountryName;
    
    return personResponse;
}
 

৬. The Final AddPerson Method (As per Transcript)

সবগুলো অংশ মিলিয়ে আমাদের AddPerson মেথডটি এখন এরকম দেখাচ্ছে:

public PersonResponse AddPerson(PersonAddRequest? personAddRequest)
{
    if (personAddRequest == null) throw new ArgumentNullException(nameof(personAddRequest));
    if (string.IsNullOrEmpty(personAddRequest.PersonName)) throw new ArgumentException("Person name cannot be blank");
 
    // Convert DTO to Model
    Person person = personAddRequest.ToPerson();
    
    // Generate new ID
    person.PersonID = Guid.NewGuid();
    
    // Insert into Data Store
    _persons.Add(person);
 
    // Convert Model to Response DTO using helper method
    return ConvertPersonToPersonResponse(person);
}
 

৭. The Failed Test Case Context (Priority: 5/10)

কোড লেখার পর টেস্ট রান করলে দেখা যায়:

  • AddPerson_NullPersonAddRequest: Pass
  • AddPerson_PersonNameIsNull: Pass
  • AddPerson_ProperPersonDetails: Fail

Why it failed: তৃতীয় টেস্টটির শেষ অংশে আমরা চেক করেছিলাম যে GetAllPersons() মেথডের লিস্টে নতুন Person টি আছে কিনা। যেহেতু আমরা এখনো GetAllPersons মেথডটি ইমপ্লিমেন্ট করিনি (সেখানে throw new NotImplementedException(); লেখা আছে), তাই এই টেস্টটি ফেইল করেছে। এটি TDD এর একটি স্বাভাবিক অংশ। পরবর্তী লেকচারগুলোতে এটি ঠিক হয়ে যাবে।


🚀 Modern C# (.NET 10 Update) & Smarter Approach

1. Better Null Checking: আধুনিক C#-এ if (personAddRequest == null) লেখার চেয়ে ArgumentNullException.ThrowIfNull() ব্যবহার করা বেশি স্ট্যান্ডার্ড।

// C# 10+ Standard
ArgumentNullException.ThrowIfNull(personAddRequest);
 

2. Handling Multiple Validations: লেকচারার একদম শেষে বলেছেন যে, অনেকগুলো ফিল্ডের জন্য অনেকগুলো if স্টেটমেন্ট লেখা বিরক্তিকর (cumbersome)। মডার্ন .NET এবং C#-এ আমরা Data Annotations (যেমন [Required], [EmailAddress]) এবং Validator.TryValidateObject ব্যবহার করি, যা আগামী লেকচারে শেখানো হবে। অথবা রিয়েল-ওয়ার্ল্ড এন্টারপ্রাইজ প্রজেক্টে FluentValidation লাইব্রেরি ব্যবহার করা হয়, যা লজিককে আরও ক্লিন রাখে।


🏆 Best Practices (Service Layer Implementation)

  1. Keep Services Independent: লেকচারে যেমন ICountriesService-কে ইনজেক্ট করা হয়েছে, তেমনি সব সময় ইন্টারফেস ইনজেক্ট করবেন, সরাসরি কংক্রিট ক্লাস (new CountriesService()) নয়। এতে টেস্টিং এবং মেইনটেন্যান্স সহজ হয়।
  2. Use Private Helpers: একই লজিক একাধিক মেথডে লাগলে তাকে সব সময় একটি private মেথডে আলাদা করে ফেলবেন (যেমন ConvertPersonToPersonResponse)।
  3. Always Return DTOs: Controller বা Test প্রজেক্টে কোনো অবস্থাতেই Domain Model এক্সপোজ করবেন না। Service লেয়ারের ইনপুট এবং আউটপুট সব সময় DTO (Data Transfer Object) হওয়া উচিত।