আমি আপনার এক্সপার্ট সফটওয়্যার ইঞ্জিনিয়ারিং ট্রেইনার। কোর্স আউটলাইন অনুযায়ী, আমরা এখন Section 15: xUnit testing-এর ভেতরে “Add Person - Validation” টপিকে আছি।

আগের লেকচারে আমরা দেখেছি প্রতিটি প্রপার্টির জন্য ম্যানুয়ালি if স্টেটমেন্ট লিখে ভ্যালিডেশন করা কতটা কষ্টকর। এই লেকচারে আমরা Model Validations (Data Annotations) ব্যবহার করে সেই প্রক্রিয়াটিকে কীভাবে স্মার্ট এবং রিইউজেবল করা যায় তা শিখব।

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

📝 Lecture Summary (Quick Revision)

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

  • The Problem: অনেকগুলো প্রপার্টির জন্য আলাদা আলাদা if স্টেটমেন্ট লেখা বিরক্তিকর (cumbersome) এবং error-prone।
  • Data Annotations: PersonAddRequest DTO ক্লাসের প্রপার্টিগুলোর ওপরে [Required], [EmailAddress] এর মতো Data Annotations যুক্ত করা।
  • Programmatic Validation: Service ক্লাসে Validator.TryValidateObject ব্যবহার করে পুরো অবজেক্টটিকে এক লাইনে ভ্যালিডেট করা।
  • Validation Context & Result: ভ্যালিডেশন প্রক্রিয়া চালানোর জন্য ValidationContext তৈরি করা এবং এরর মেসেজগুলো সেভ করার জন্য List<ValidationResult> ব্যবহার করা।
  • Refactoring (DRY Principle): ভ্যালিডেশন লজিকটি বারবার না লিখে, Helpers ফোল্ডারে ValidationHelper নামে একটি static ক্লাস তৈরি করে কোডটিকে রিইউজেবল করা।
  • Service Update: PersonsService-এর ভেতরে পুরনো if লজিক মুছে ফেলে শুধু ValidationHelper.ModelValidation() কল করা।

🧠 Comprehensive Breakdown

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

১. DTO-তে Data Annotations যুক্ত করা (Priority: 9/10)

Concept & Why: রিয়েল-ওয়ার্ল্ড প্রজেক্টে একটি মডেলে ২০-৩০টি প্রপার্টি থাকতে পারে এবং প্রতিটির একাধিক রুল (Rule) থাকতে পারে (যেমন: Required, Range, MaxLength)। এগুলোর জন্য আলাদা আলাদা if স্টেটমেন্ট লেখা সম্ভব নয়। তাই আমরা System.ComponentModel.DataAnnotations নেমস্পেস ব্যবহার করে DTO ক্লাসে রুলগুলো ডিক্লেয়ার করব।

// ServiceContracts/DTO/PersonAddRequest.cs
using System.ComponentModel.DataAnnotations;
 
public class PersonAddRequest
{
    [Required(ErrorMessage = "Person Name cannot be blank")]
    public string? PersonName { get; set; }
 
    [Required(ErrorMessage = "Email cannot be blank")]
    [EmailAddress(ErrorMessage = "Email should be a valid email address")]
    public string? Email { get; set; }
 
    // এটি অপশনাল, তাই কোনো Data Annotation দেওয়া হয়নি
    public DateTime? DateOfBirth { get; set; } 
    
    // ... অন্যান্য প্রপার্টিগুলো
}
 

২. Programmatic Model Validation (Priority: 10/10)

Concept & Why: সাধারণত Controller-এ Model Binding-এর সময় অটোমেটিকভাবে ভ্যালিডেশন হয় (ModelState.IsValid)। কিন্তু আমরা যেহেতু Service লেয়ারে কাজ করছি এবং Unit Test লিখছি (যেখানে কোনো Controller নেই), তাই আমাদের ম্যানুয়ালি কোড লিখে (Programmatically) এই Data Annotations গুলোকে ট্রিগার করতে হবে।

এর জন্য .NET-এর বিল্ট-ইন Validator ক্লাস ব্যবহার করা হয়।

// PersonsService.cs এর ভেতরের লজিক (যা পরে Refactor করা হবে)
 
// ১. Validation Context তৈরি করা (কোন অবজেক্ট ভ্যালিডেট হবে তা বলে দেওয়া)
ValidationContext validationContext = new ValidationContext(personAddRequest);
 
// ২. এরর লিস্ট তৈরি করা (যেখানে ভুলগুলো জমা হবে)
List<ValidationResult> validationResults = new List<ValidationResult>();
 
// ৩. Validator দিয়ে চেক করা
bool isValid = Validator.TryValidateObject(
    personAddRequest, 
    validationContext, 
    validationResults, 
    true // **Critical:** true দিলে সব রুল চেক করবে। false দিলে শুধু [Required] চেক করবে।
);
 
// ৪. যদি ভ্যালিড না হয়, তবে Exception থ্রো করা
if (!isValid)
{
    // validationResults থেকে প্রথম এরর মেসেজটি বের করে আনা
    string? errorMessage = validationResults.FirstOrDefault()?.ErrorMessage;
    throw new ArgumentException(errorMessage);
}
 

৩. Refactoring & Reusability: ValidationHelper তৈরি (Priority: 10/10)

Concept & Why: ওপরে লেখা ভ্যালিডেশন কোডটি শুধুমাত্র AddPerson-এর জন্য কাজ করবে। কিন্তু ভবিষ্যতে যখন আমরা UpdatePerson মেথড বানাবো, তখনও ঠিক একই কোড লিখতে হবে। কোড রিপিট করা (WET - We Enjoy Typing) সফটওয়্যার ইঞ্জিনিয়ারিংয়ের চরম ভুল। আমাদের DRY (Don’t Repeat Yourself) প্রিন্সিপাল ফলো করতে হবে।

তাই আমরা Services প্রজেক্টে Helpers নামের একটি ফোল্ডার তৈরি করে সেখানে ValidationHelper নামের একটি static ক্লাস বানিয়েছি। প্যারামিটার হিসেবে স্পেসিফিক PersonAddRequest না দিয়ে সাধারণ object দিয়েছি, যাতে যেকোনো ক্লাস এটি ব্যবহার করতে পারে।

// Services/Helpers/ValidationHelper.cs
using System.ComponentModel.DataAnnotations;
 
namespace Services.Helpers
{
    public static class ValidationHelper
    {
        public static void ModelValidation(object obj)
        {
            ValidationContext validationContext = new ValidationContext(obj);
            List<ValidationResult> validationResults = new List<ValidationResult>();
 
            bool isValid = Validator.TryValidateObject(obj, validationContext, validationResults, true);
 
            if (!isValid)
            {
                throw new ArgumentException(validationResults.FirstOrDefault()?.ErrorMessage);
            }
        }
    }
}
 

৪. Updating the PersonsService (Priority: 8/10)

Concept & Why: এখন আমাদের AddPerson মেথডটি আগের চেয়ে অনেক বেশি ক্লিন এবং ছোট হয়ে গেছে। আমরা জাস্ট এক লাইনে ValidationHelper কল করব।

// Services/PersonsService.cs
public PersonResponse AddPerson(PersonAddRequest? personAddRequest)
{
    if (personAddRequest == null) throw new ArgumentNullException(nameof(personAddRequest));
 
    // এক লাইনে পুরো অবজেক্টের সব Data Annotation ভ্যালিডেট করা হচ্ছে
    ValidationHelper.ModelValidation(personAddRequest);
 
    // ... বাকি Insert লজিক আগের মতোই থাকবে
}
 

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

লেকচারে দেখানো Validator.TryValidateObject এপ্রোচটি শতভাগ সঠিক এবং .NET 10 এও চমৎকারভাবে কাজ করে। কিন্তু রিয়েল-ওয়ার্ল্ড এন্টারপ্রাইজ প্রজেক্টে (বিশেষ করে Clean Architecture বা আধুনিক API ডেভেলপমেন্টে), Service লেয়ারে Data Annotations ব্যবহার করাকে কিছুটা “Old School” বা পুরোনো পদ্ধতি মনে করা হয়।

**The Modern Industry Standard: FluentValidation** বর্তমানে .NET কমিউনিটিতে ভ্যালিডেশনের জন্য সবচেয়ে জনপ্রিয় থার্ড-পার্টি লাইব্রেরি হলো FluentValidation

  • কেন ভালো? Data Annotations আপনার DTO ক্লাসকে নোংরা করে ফেলে। FluentValidation-এ আপনার DTO একদম ক্লিন থাকে এবং ভ্যালিডেশন লজিক সম্পূর্ণ আলাদা একটি ক্লাসে লেখা হয়। এটি Dependency Injection সাপোর্ট করে।

.NET 10 Modern Code Example with FluentValidation:

// 1. Clean DTO (No Data Annotations)
public record PersonAddRequest(string PersonName, string Email);
 
// 2. Separate Validation Class
public class PersonAddRequestValidator : AbstractValidator<PersonAddRequest>
{
    public PersonAddRequestValidator()
    {
        RuleFor(x => x.PersonName).NotEmpty().WithMessage("Person Name cannot be blank");
        RuleFor(x => x.Email).NotEmpty().EmailAddress().WithMessage("Valid email is required");
    }
}
 
// 3. In Service (Injected via DI)
public PersonResponse AddPerson(PersonAddRequest request)
{
    _validator.ValidateAndThrow(request); // One line modern validation!
    // ... 
}
 

🏆 Best Practices for Object Validation

  1. Keep Helper Classes Static: লেকচারে যেমন ValidationHelper ক্লাসটিকে static করা হয়েছে, এটি বেস্ট প্র্যাকটিস। কারণ এই ক্লাসের নিজস্ব কোনো স্টেট (State) বা ডেটা সেভ করে রাখার প্রয়োজন নেই।
  2. DTOs for Validation: সব সময় ভ্যালিডেশন রুলস (Data Annotations) DTO ক্লাসের ওপরে লিখবেন। ডাটাবেসের মেইন Entity (Domain Model) ক্লাসের ওপরে কখনোই Data Annotations দিয়ে ভ্যালিডেশন করা উচিত নয়, কারণ ডাটাবেসের লজিক এবং ইউজার ইনপুটের লজিক ভিন্ন হতে পারে।
  3. Validate All Properties: Validator.TryValidateObject কল করার সময় শেষের প্যারামিটারটি সব সময় true রাখবেন। না হলে এটি শুধুমাত্র [Required] ট্যাগগুলো চেক করবে, বাকিগুলো (যেমন [EmailAddress], [Range]) ইগনোর করে যাবে!