হাসিব, তোমার দেওয়া লেকচার ট্রান্সক্রিপ্টটির বিস্তারিত বিশ্লেষণ নিচে দেওয়া হলো। এই লেকচারে Model Validation-এর আরেকটি চমৎকার পদ্ধতি— IValidatableObject ইন্টারফেস নিয়ে আলোচনা করা হয়েছে। যখন তোমার রিকোয়ারমেন্ট শুধু একটি নির্দিষ্ট মডেল ক্লাসের জন্য হয় এবং অন্য কোথাও রিইউজ করার দরকার না থাকে, তখন Custom Attribute না বানিয়ে সরাসরি এই ইন্টারফেস ব্যবহার করে খুব সহজে ভ্যালিডেশন করা যায়।
📝 Quick Summary for Revision
ভবিষ্যতে দ্রুত রিভিশন দেওয়ার জন্য মূল পয়েন্টগুলো নিচে লিস্ট করা হলো:
- The Scenario: যখন একাধিক প্রোপার্টির ওপর নির্ভর করে কোনো ভ্যালিডেশন লজিক লিখতে হয় (যেমন: Date of Birth অথবা Age—যেকোনো একটি অবশ্যই দিতে হবে), কিন্তু সেই লজিকটি অন্য কোনো মডেলে রিইউজ করার প্রয়োজন নেই।
- The Solution: মডেল ক্লাসটিকে
IValidatableObjectইন্টারফেস থেকে ইনহেরিট করানো। - The Execution Block: এই ইন্টারফেসের
Validateমেথডটি ইমপ্লিমেন্ট করতে হয়। - Execution Order: মডেলের অন্যান্য Attribute-based ভ্যালিডেশন (যেমন
[Required],[Range]) সম্পূর্ণ সফলভাবে পাস হওয়ার পরই কেবল এইValidateমেথডটি কল হয়। - Yield Return: যেহেতু
Validateমেথডটি একাধিক Error রিটার্ন করতে পারে (IEnumerable<ValidationResult>), তাই ভ্যালু রিটার্ন করার সময়yield returnব্যবহার করতে হয়। nameofOperator: হার্ডকোড করে প্রোপার্টির নাম স্ট্রিং হিসেবে লেখার বদলে C#-এরnameof(PropertyName)ব্যবহার করা বেস্ট প্র্যাকটিস।
🧠 Comprehensive Breakdown
এখানে লেকচারের প্রতিটি কনসেপ্ট, লজিক এবং কোড ইমপ্লিমেন্টেশন ধাপে ধাপে বিস্তারিতভাবে ব্যাখ্যা করা হলো।
1. Why IValidatableObject? (Class-Level Validation) [Priority: 10/10]
The Problem:
ধরে নাও, তোমার Person মডেলে দুটি ফিল্ড আছে: DateOfBirth এবং Age। ইউজারের রিকোয়ারমেন্ট হলো, তাকে এই দুটির যেকোনো একটি অবশ্যই দিতে হবে। সে চাইলে শুধু বয়স (Age) দিতে পারে, অথবা শুধু জন্মতারিখ (DateOfBirth) দিতে পারে। কিন্তু সে যদি দুটিই ফাঁকা রাখে, তবে সেটি Error হবে।
আগের লেকচারে আমরা দেখেছি যে, এই ধরনের একাধিক ফিল্ডের ভ্যালিডেশন করতে Custom Attribute বানাতে হয় এবং সেখানে Reflection ব্যবহার করতে হয়, যা বেশ কমপ্লেক্স এবং সময়সাপেক্ষ।
The Solution:
যদি তুমি জানো যে এই লজিকটি শুধুমাত্র Person ক্লাসের জন্যই লাগবে, অন্য কোনো ফর্মে বা মডেলে এই লজিক লাগবে না—তাহলে কষ্ট করে Custom Attribute বানানোর দরকার নেই। তুমি সরাসরি মডেল ক্লাসটিকে IValidatableObject ইন্টারফেসের আন্ডারে নিয়ে এসে ক্লাস লেভেলেই ভ্যালিডেশন লিখে ফেলতে পারো।
2. Implementing the Interface & Validate Method [Priority: 10/10]
প্রথমে Person ক্লাসকে IValidatableObject ইন্টারফেসটি ইমপ্লিমেন্ট করতে হবে। এই ইন্টারফেসে মাত্র একটি মেথড আছে: Validate।
(VS Code Shortcut: IValidatableObject লেখার পর এর ওপর মাউস রেখে Ctrl + . (বা Quick Fix বাল্ব আইকনে ক্লিক) চাপলে “Implement Interface” অপশন আসবে। এটি সিলেক্ট করলে অটোমেটিকভাবে Validate মেথডটি ক্লাসের নিচে তৈরি হয়ে যাবে।)
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace YourProjectName.Models
{
// Inheriting from IValidatableObject
public class Person : IValidatableObject
{
public DateTime? DateOfBirth { get; set; }
public int? Age { get; set; }
// ... other properties like PersonName, Email, etc.
// The method generated by the interface
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// We will write the custom validation logic here
}
}
}
3. Writing the Custom Logic (No Reflection Needed!) [Priority: 10/10]
Custom Attribute-এ আমরা validationContext থেকে Reflection দিয়ে প্রোপার্টির ভ্যালু বের করেছিলাম। কিন্তু যেহেতু আমরা এখন খোদ Person ক্লাসের ভেতরেই আছি, তাই সরাসরি প্রোপার্টিগুলোকে কল করলেই তাদের ভ্যালু পাওয়া যাবে!
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Requirement: Either DateOfBirth or Age MUST be supplied.
// If BOTH do not have a value, it's an error.
if (DateOfBirth.HasValue == false && Age.HasValue == false)
{
// Yield return is required because the return type is IEnumerable
yield return new ValidationResult(
"Either Date of Birth or Age must be supplied.",
new string[] { nameof(Age), nameof(DateOfBirth) }
);
}
}
4. The yield return Keyword [Priority: 9/10]
Why do we use it?
খেয়াল করো, মেথডের রিটার্ন টাইপ হলো IEnumerable<ValidationResult>। এর মানে হলো, তুমি চাইলে একসাথে অনেকগুলো Error মেসেজের লিস্ট রিটার্ন করতে পারো।
C#-এ যখন তুমি একটি লিস্ট বা কালেকশন তৈরি না করেই মেমোরি-এফিশিয়েন্টভাবে একাধিক ভ্যালু রিটার্ন করতে চাও, তখন yield return ব্যবহার করা হয়। এটি মেথডের এক্সিকিউশন স্টেট মনে রাখে এবং যতবার লুপ চলে ততবার নতুন ভ্যালু রিটার্ন করে।
যদি তোমার মডেলে আরও কিছু লজিক থাকে, তুমি সহজেই পরপর লিখতে পারো:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Logic 1
if (!DateOfBirth.HasValue && !Age.HasValue)
{
yield return new ValidationResult("Either DoB or Age is required.");
}
// Logic 2 (Example)
if (Age.HasValue && Age < 18)
{
yield return new ValidationResult("You must be 18+ to register.");
}
}
5. The nameof Operator (Best Practice) [Priority: 8/10]
Why do we need this?
লেকচারে দেখানো হয়েছে যে Error মেসেজের পর নির্দিষ্ট ফিল্ডের নাম বলে দেওয়ার জন্য একটি স্ট্রিং অ্যারে পাস করা হয়: new string[] { "Age" }।
কিন্তু হার্ডকোড করে ডাবল কোটেশনে "Age" লেখাটা ঝুঁকিপূর্ণ। ভবিষ্যতে যদি তুমি প্রোপার্টির নাম পরিবর্তন করে PresentAge করো, তাহলে স্ট্রিংয়ের ভেতরের নাম অটোমেটিক চেঞ্জ হবে না, যার ফলে রানটাইমে বাগ দেখা দেবে।
এই সমস্যা সমাধানে C#-এর nameof() অপারেটর ব্যবহার করা হয়। nameof(Age) লিখলে কম্পাইলার নিজেই সেটিকে "Age" স্ট্রিংয়ে কনভার্ট করে নেয়। আর তুমি প্রোপার্টির নাম পরিবর্তন করলে এটি অটোমেটিকভাবে আপডেট হয়ে যায়।
6. Execution Order (Important Catch) [Priority: 9/10]
লেকচারের মাঝখানে ইন্সট্রাক্টর একটি খুব গুরুত্বপূর্ণ কথা বলেছেন। তুমি যখন কোড রান করে ব্রেকপয়েন্ট (Breakpoint) চেক করবে, দেখবে অনেক সময় Validate মেথডটি কলই হচ্ছে না!
কেন এমন হয়?
ফ্রেমওয়ার্কের নিয়ম হলো: প্রথমে সে Model-এর ওপর বসানো Attribute-গুলো (যেমন: [Required], [EmailAddress]) চেক করে। যদি সেগুলোর কোনো একটিতেও Error পাওয়া যায়, তবে সে আর IValidatableObject-এর Validate মেথড কল করে না।
সব Attribute ভ্যালিডেশন সফলভাবে পাস হওয়ার পরই কেবল সবশেষে Validate মেথডটি এক্সিকিউট হয়। তাই এই মেথডটিকে টেস্টিং করার সময় খেয়াল রাখতে হবে যেন মডেলের বাকি সব ফিল্ডে সঠিক ডেটা দেওয়া হয়।
(নোট: লেকচারে PersonName এর Regular Expression-এ একটি ছোট ভুলের কথা বলা হয়েছে, যেখানে * (asterisk) মিসিং ছিল, যার কারণে সেটি শুধু একটি ক্যারেক্টার নিচ্ছিল। টেস্টিংয়ের সময় ওই বাগটি ফিক্স করা হয়েছে।)
🚀 Best Practices & .NET 10 / C# 13 Updates
**1. Best Practice: Minimal API and IValidatableObject**
.NET 8+ এবং .NET 10-এ Minimal API ব্যবহার করার সময়, ফ্রেমওয়ার্ক বাই-ডিফল্ট IValidatableObject এর Validate মেথডটিকে কল করে না, যদি না তুমি নিজে থেকে ইনজেক্ট করো বা থার্ড-পার্টি ফিল্টার (যেমন: MiniValidation) ব্যবহার করো।
Minimal API-তে সাধারণত FluentValidation ব্যবহার করাই এখন স্ট্যান্ডার্ড প্র্যাকটিস। তবে তুমি যদি Controller ([ApiController]) ব্যবহার করো, তবে IValidatableObject অটোমেটিকভাবে আগের মতোই কাজ করবে।
2. Modern C# Equivalent with FluentValidation
যেহেতু তুমি ক্লিন আর্কিটেকচার এবং মডার্ন .NET নিয়ে কাজ করছো, এই একই কাজ IValidatableObject ছাড়া FluentValidation দিয়ে আরও সুন্দরভাবে করা যায়।
// The Model remains completely clean
public class Person
{
public DateTime? DateOfBirth { get; set; }
public int? Age { get; set; }
}
// FluentValidation automatically handles the complex logic
using FluentValidation;
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
// Custom rule: Either DateOfBirth or Age must be provided
RuleFor(x => x)
.Must(x => x.DateOfBirth.HasValue || x.Age.HasValue)
.WithMessage("Either Date of Birth or Age must be supplied.")
// Targeting the specific fields for the error message
.OverridePropertyName(nameof(Person.Age));
}
}
কখন কোনটা ব্যবহার করবে?
IValidatableObject: যদি প্রজেক্ট ছোট হয়, কোনো এক্সট্রা লাইব্রেরি ইন্সটল করতে না চাও এবং লজিকটি শুধু একটি নির্দিষ্ট ক্লাসের জন্যই হয়।- Custom Validation Attribute: যদি একই ভ্যালিডেশন লজিক বিভিন্ন ক্লাসের একাধিক ফিল্ডে বারবার ব্যবহার (Reuse) করতে হয়।
- FluentValidation (Industry Standard): যদি প্রজেক্ট বড় হয়, ক্লিন আর্কিটেকচার ফলো করো এবং মডেল ক্লাসকে ১০০% ক্লিন রাখতে চাও।