অবশ্যই হাসিব। এই লেকচারটি অত্যন্ত গুরুত্বপূর্ণ, কারণ এখানে Model Validation-এর অ্যাডভান্সড একটি টপিক—Multiple Properties Validation (Custom) নিয়ে আলোচনা করা হয়েছে। চলো প্রথমে এর একটি কুইক সামারি দেখে নিই।


📝 Quick Summary for Revision

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

  • The Goal: একটি মডেলের দুটি ভিন্ন ফিল্ডের মধ্যে তুলনা করা। যেমন: FromDate অবশ্যই ToDate এর চেয়ে ছোট (পুরোনো) হতে হবে।

  • The Concept: এর জন্য একটি Custom Validation Attribute তৈরি করতে হবে যা শুধুমাত্র একটি প্রোপার্টি নয়, বরং মডেলের আরেকটি প্রোপার্টির ভ্যালুও রিড করতে পারে।

  • Constructor Injection: কোন প্রোপার্টির সাথে তুলনা করতে হবে (যেমন FromDate), তার নাম Custom Attribute-এর Constructor-এর মাধ্যমে পাস করতে হয়।

  • ValidationContext: এই অবজেক্টটি পুরো মডেলের (যেমন Person ক্লাস) অ্যাক্সেস দেয়।

  • validationContext.ObjectInstance: এটি মূলত মডেলের ইনস্ট্যান্স (যেখানে ইউজারের সব ডেটা আছে)।

  • validationContext.ObjectType: এটি মডেলের টাইপ ইনফরমেশন দেয়, যা Reflection-এ ব্যবহার হয়।

  • Reflection in C#: যেহেতু আমরা ডাইনামিকভাবে অন্য একটি প্রোপার্টির ভ্যালু রিড করতে চাচ্ছি, তাই System.Reflection.PropertyInfo ব্যবহার করে GetValue() মেথডের মাধ্যমে FromDate এর ভ্যালু বের করা হয়েছে।

  • Error Assignment: ValidationResult-এ আমরা কোন কোন ফিল্ডের জন্য Error দেখাতে চাই, তা একটি অ্যারেকে (Array) পাস করে নির্দিষ্ট করে দিতে পারি।


🧠 Comprehensive Breakdown

এখানে লেকচারের প্রতিটি কনসেপ্ট, লজিক এবং কোড ইমপ্লিমেন্টেশন ধাপে ধাপে বিস্তারিতভাবে ব্যাখ্যা করা হলো।

1. The Requirement & The Problem [Priority: 10/10]

Why do we need this? ধরে নাও তোমার একটি ট্রানজেকশন খোঁজার ফর্ম আছে, যেখানে ইউজারকে FromDate এবং ToDate দিতে হয়। লজিক্যালি, FromDate (শুরুর তারিখ) সবসময় ToDate (শেষের তারিখ) এর আগে বা সমান হতে হবে।

আগের লেকচারে আমরা দেখেছি কিভাবে একটিমাত্র ফিল্ড (যেমন শুধু DateOfBirth) ভ্যালিডেট করতে হয়। কিন্তু এখানে আমাদের এমন একটি Attribute বানাতে হবে যা ToDate-এর ওপর বসানো থাকবে, কিন্তু সে নিজে থেকেই FromDate-এর ডেটা নিয়ে এসে তুলনা করবে।

2. Using the Attribute in the Model [Priority: 8/10]

প্রথমেই আমরা চিন্তা করি Attribute-টি মডেলে কীভাবে ব্যবহার করব। আমরা DateRangeValidator নামে একটি Attribute বানাবো এবং সেটিকে ToDate এর ওপর বসাবো।

public class Person
{
    public DateTime? FromDate { get; set; }
 
    // Passing the name of the property we want to compare with ("FromDate")
    [DateRangeValidator("FromDate", ErrorMessage = "From Date should be older than or equal to To Date.")]
    public DateTime? ToDate { get; set; }
}
 

3. Creating the Custom Attribute Class [Priority: 10/10]

এবার আমরা CustomValidators ফোল্ডারে DateRangeValidatorAttribute.cs নামের ক্লাসটি তৈরি করব এবং ValidationAttribute থেকে ইনহেরিট করব।

যেহেতু আমরা মডেল থেকে FromDate নামটিকে স্ট্রিং হিসেবে পাস করছি, আমাদের Attribute ক্লাসে একটি Constructor লাগবে যা এই স্ট্রিংটিকে রিসিভ করবে।

using System;
using System.ComponentModel.DataAnnotations;
using System.Reflection; // Required for Reflection
 
namespace YourProjectName.CustomValidators
{
    public class DateRangeValidatorAttribute : ValidationAttribute
    {
        // Property to store the name of the other property (e.g., "FromDate")
        public string OtherPropertyName { get; set; }
 
        // Constructor to receive the property name
        public DateRangeValidatorAttribute(string otherPropertyName)
        {
            OtherPropertyName = otherPropertyName;
        }
 
        // ... we will override IsValid here
    }
}
 

4. The Magic of Reflection & ValidationContext [Priority: 10/10]

এটি এই লেকচারের সবচেয়ে ক্রিটিক্যাল পার্ট। IsValid মেথডে আমরা দুটি প্যারামিটার পাই:

  1. value: এটি হলো ওই প্রোপার্টির নিজস্ব ভ্যালু যার ওপর Attribute-টি বসানো আছে। আমাদের ক্ষেত্রে এটি হলো ToDate-এর ভ্যালু।
  2. validationContext: এটি হলো পুরো মডেল অবজেক্টের কন্টেক্সট।

সমস্যাটা কোথায়? আমরা validationContext.ObjectInstance দিয়ে পুরো Person অবজেক্টটি পাই। কিন্তু এর ডেটা টাইপ থাকে System.Object। তাই আমরা সরাসরি validationContext.ObjectInstance.FromDate লিখতে পারি না।

সমাধান: Reflection Reflection হলো C#-এর এমন একটি পাওয়ারফুল ফিচার যা রানটাইমে (Runtime) কোনো অবজেক্টের মেটাডেটা বা প্রোপার্টি রিড করতে পারে, এমনকি যদি তুমি সেই অবজেক্টের আসল টাইপ নাও জানো।

protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
    if (value != null)
    {
        // 1. Get the value of ToDate (the current property)
        DateTime toDate = (DateTime)value;
 
        // 2. REFLECTION: Ask the Model Type to give the reference of the "FromDate" property
        PropertyInfo? otherProperty = validationContext.ObjectType.GetProperty(OtherPropertyName);
 
        if (otherProperty != null)
        {
            // 3. REFLECTION: Extract the actual value of "FromDate" from the Model Object
            DateTime fromDate = (DateTime)otherProperty.GetValue(validationContext.ObjectInstance)!;
 
            // 4. THE LOGIC: Compare both dates
            if (fromDate > toDate)
            {
                // Validation Failed
                return new ValidationResult(
                    ErrorMessage ?? "From Date must be older than To Date", 
                    new string[] { OtherPropertyName, validationContext.MemberName! }
                );
            }
            
            // Validation Passed
            return ValidationResult.Success;
        }
        
        return null; // If other property is not found
    }
    return null; // If value is null
}
 

5. Returning Validation Results for Specific Fields [Priority: 7/10]

ValidationResult রিটার্ন করার সময় আমরা খেয়াল করলে দেখব, সেখানে একটি অ্যারে (Array) পাস করা হয়েছে: new string[] { OtherPropertyName, validationContext.MemberName! }

এর মানে কী? ধরো, ইউজার ভুল ডেটা দিয়েছে। এখন Error মেসেজটি ফ্রন্টএন্ডের কোন টেক্সট বক্সের নিচে দেখাবে? FromDate এর নিচে নাকি ToDate এর নিচে? এই অ্যারে দিয়ে আমরা বলে দিচ্ছি যে, “এই Error-টি FromDate এবং ToDate—দুটোর সাথেই সম্পর্কিত।” এর ফলে ফ্রন্টএন্ডে বা API রেসপন্সে এই Error মেসেজটি ওই দুই ফিল্ডের জন্যই লিস্টেড হবে। (লেকচারে দেখানো হয়েছে যে, Postman-এ একই মেসেজ দুইবার এসেছে, কারণ এটি দুই ফিল্ডেই ট্যাগ হয়েছে)।


🚀 Best Practices & .NET 10 / C# 13 Updates

1. Best Practice: ValidationContext.MemberName (Code Cleanup) লেকচারে validationContext.MemberName ব্যবহার করা হয়েছে। C# 10+ এর Nullable রেফারেন্স টাইপ চালু থাকার কারণে MemberName এর শেষে একটি ! (null-forgiving operator) দেওয়া উচিত, যাতে কম্পাইলার ওয়ার্নিং না দেয় (যেভাবে উপরের কোডে আমি দিয়েছি)।

2. Modern Architecture Shift: Why FluentValidation is Better for Cross-Property Checks হাসিব, তুমি যেহেতু .NET নিয়ে গভীর কাজ করছো, তোমার জানা উচিত যে, Cross-Property Validation (একাধিক ফিল্ডের তুলনা) এর জন্য Reflection ব্যবহার করাটা পারফরম্যান্সের দিক থেকে বেশ স্লো এবং ক্লিন আর্কিটেকচারে এটি একটি Anti-pattern হিসেবে ধরা হয়।

ইন্ডাস্ট্রি এখন এই ধরনের কাজের জন্য Data Annotations ব্যবহার করে না। এর বদলে FluentValidation ব্যবহার করা হয়। FluentValidation-এ কোনো Reflection লাগে না এবং এটি টাইপ-সেইফ (Type-safe)।

.NET 8/10 এ FluentValidation দিয়ে এই একই কাজ মাত্র ২ লাইনে করা যায়:

using FluentValidation;
 
public class Person
{
    public DateTime? FromDate { get; set; }
    public DateTime? ToDate { get; set; }
}
 
public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        // NO REFLECTION NEEDED! Super fast and clean.
        RuleFor(x => x.FromDate)
            .LessThanOrEqualTo(x => x.ToDate)
            .WithMessage("From Date must be older than or equal to To Date.")
            // Apply this rule only if both dates have values
            .When(x => x.FromDate.HasValue && x.ToDate.HasValue); 
    }
}
 

কখন কোনটা ব্যবহার করবে?

  • যদি সিম্পল ভ্যালিডেশন হয় (যেমন: Required, StringLength), তবে Data Annotations বেস্ট।
  • যদি দুটো প্রোপার্টির তুলনা করতে হয় বা কমপ্লেক্স লজিক থাকে, তবে FluentValidation বেস্ট।

লেকচারের এই Reflection-এর কনসেপ্টটি C#-এর গভীরের একটি জিনিস। এটি বুঝতে একটু সময় লাগতে পারে। কোনো পার্টে কনফিউশন থাকলে জানাতে পারো!