Model Binding, DTOs & Validation

লেসন ১৮ / ৫০

DTO (Data Transfer Object) Pattern ⭐

API request/response এর জন্য আলাদা class — Entity directly expose না করা।

// Entity (DB)
public class User {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }    // sensitive!
    public DateTime CreatedAt { get; set; }
}

// Request DTO
public class CreateUserDto {
    public string Name { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

// Response DTO
public class UserDto {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    // PasswordHash exclude!
}

Data Annotations Validation

using System.ComponentModel.DataAnnotations;

public class CreateUserDto {
    [Required(ErrorMessage = "নাম দিন")]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [StringLength(50, MinimumLength = 8)]
    [RegularExpression(@"^(?=.*[A-Z])(?=.*[0-9]).+$",
        ErrorMessage = "Password must have uppercase + digit")]
    public string Password { get; set; }

    [Range(18, 100)]
    public int Age { get; set; }

    [Phone]
    public string Phone { get; set; }

    [Url]
    public string Website { get; set; }

    [Compare("Password")]
    public string ConfirmPassword { get; set; }
}

Auto Validation ([ApiController])

[ApiController]    // ← validation auto trigger
[Route("api/[controller]")]
public class UsersController : ControllerBase {

    [HttpPost]
    public IActionResult Create(CreateUserDto dto) {
        // ModelState invalid হলে আগেই 400 return হয়েছে
        // এখানে আসা মানে valid

        var user = new User { Name = dto.Name, Email = dto.Email };
        // ...
        return Created($"/api/users/{user.Id}", user);
    }
}

// 400 response আসবে:
// {
//   "errors": {
//     "Email": ["The Email field is required"],
//     "Password": ["Password must have uppercase + digit"]
//   }
// }

Manual Validation

if (!ModelState.IsValid) {
    return BadRequest(ModelState);
}

Custom Validation Attribute

public class FutureDateAttribute : ValidationAttribute {
    protected override ValidationResult IsValid(object value, ValidationContext ctx) {
        if (value is DateTime date && date > DateTime.Now)
            return ValidationResult.Success;
        return new ValidationResult("Date must be future");
    }
}

public class EventDto {
    [FutureDate]
    public DateTime EventDate { get; set; }
}

FluentValidation (3rd party) ⭐

# dotnet add package FluentValidation.AspNetCore

public class CreateUserValidator : AbstractValidator<CreateUserDto> {
    public CreateUserValidator() {
        RuleFor(x => x.Name).NotEmpty().Length(2, 100);
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(8)
            .Matches("[A-Z]").WithMessage("Need uppercase")
            .Matches("[0-9]").WithMessage("Need digit");
        RuleFor(x => x.Age).InclusiveBetween(18, 100);
    }
}

// Program.cs
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserValidator>();

AutoMapper (Entity ↔ DTO)

# dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

public class MappingProfile : Profile {
    public MappingProfile() {
        CreateMap<User, UserDto>();
        CreateMap<CreateUserDto, User>();
    }
}

// Use
public class UsersController : ControllerBase {
    private readonly IMapper _mapper;
    public UsersController(IMapper mapper) => _mapper = mapper;

    public ActionResult<UserDto> Get(int id) {
        var user = _db.Users.Find(id);
        return _mapper.Map<UserDto>(user);
    }
}