IValidatableObject in .NET
Maybe our blog is turning into a code blog? Nah. All of my creative writing is bent towards rabbinical school these days, is all, and I absolutely don’t want to write about politics or everything since October 7th.
For the longest time, I’ve been using FluentValidation as my go-to for complex validation on objects. Somehow, I have not known about IValidatableObject. In the vast space between use cases, for me I’ve either stopped at validation attributes like [Required] or [MaxLength(x)].
IValidatableObject is part of System.ComponentModel.DataAnnotations, and apparently has been for a very long time. I think we run into this scenario as devs quite often, where something exists and somehow we missed it until someone else points it out. Ah, well…lifelong love of learning means sometimes, you’re humbled.
To be clear, for complex validation, I suspect there will always be a place for FluentValidation in my heart. Up front, there are behaviors for IValidatableObject that are not desirable. If I’m primarily using it for DTOs in my infrastructure, it’s one thing, but if validations attributes on properties fail, Validate will not fire, so you will not get a complete validation picture returned. It’s something to know going into its use.
It’s very simple to implement, which makes it attractive all the same, so long as you understand its limitations.
I have a use case for an international system where I need to validate states/provinces based on country. There may be better ways to do this, but I’m trying to make this particular system as lean as possible. It works for what I need, checking against shared constants.
My code follows. Hope this helps!
public class AddressContactRequest : IValidatableObject
{
public string MailingAddressLine1 { get; set; } = null!;
public string MailingAddressLine2 { get; set; } = null!;
public string MailingAddressLine3 { get; set; } = null!;
public string MailingAddressCity { get; set; } = null!;
public string MailingAddressState { get; set; } = null!;
public string MailingAddressZip { get; set; } = null!;
public string MailingAddresCountry { get; set; } = null!;
public string BillingAddressLine1 { get; set; } = null!;
public string BillingAddressLine2 { get; set; } = null!;
public string BillingAddressLine3 { get; set; } = null!;
public string BillingAddressCity { get; set; } = null!;
public string BillingAddressState { get; set; } = null!;
public string BillingAddressZip { get; set; } = null!;
public string BillingAddressCountry { get; set; } = null!;
public string Phone1 { get; set; } = null!;
public string Phone2 { get; set; } = null!;
public string Phone3 { get; set; } = null!;
public string Fax { get; set; } = null!;
public string PointOfContactName { get; set; } = null!;
public string Email { get; set; } = null!;
public string WebsiteUrl { get; set; } = null!;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(BillingAddressCountry) && !AddressConstants.COUNTRIES.Contains(BillingAddressCountry))
{
yield return new ValidationResult("Billing address country must be United States, Canada, Australia, Mexico, or Israel", new[] { nameof(BillingAddressCountry) });
}
if (!string.IsNullOrWhiteSpace(MailingAddresCountry) && !AddressConstants.COUNTRIES.Contains(MailingAddresCountry))
{
yield return new ValidationResult("Mailing address country must be United States, Canada, Australia, Mexico, or Israel", new[] { nameof(MailingAddresCountry) });
}
if (MailingAddresCountry == "USA" && !AddressConstants.USSTATES.Contains(MailingAddressState))
{
yield return new ValidationResult("United States addresses require a valid state.", new[] { nameof(MailingAddressState)});
}
if (BillingAddressCountry == "USA" && !AddressConstants.USSTATES.Contains(BillingAddressState))
{
yield return new ValidationResult("United States addresses require a valid state.", new[] { nameof(BillingAddressState)});
}
if (MailingAddresCountry == "CAN" && !AddressConstants.CANSTATES.Contains(MailingAddressState))
{
yield return new ValidationResult("Canada addresses require a valid province or territory.", new[] { nameof(MailingAddressState)});
}
if (BillingAddressCountry == "CAN" && !AddressConstants.CANSTATES.Contains(BillingAddressState))
{
yield return new ValidationResult("Canada addresses require a valid province or territory.", new[] { nameof(BillingAddressState)});
}
if (MailingAddresCountry == "AUS" && !AddressConstants.AUSSTATES.Contains(MailingAddressState))
{
yield return new ValidationResult("Australia addresses require a valid state or territory.", new[] { nameof(MailingAddressState)});
}
if (BillingAddressCountry == "AUS" && !AddressConstants.AUSSTATES.Contains(BillingAddressState))
{
yield return new ValidationResult("Australia addresses require a valid state or territory.", new[] { nameof(BillingAddressState)});
}
if (MailingAddresCountry == "MEX" && !AddressConstants.MEXSTATES.Contains(MailingAddressState))
{
yield return new ValidationResult("Mexico addresses require a valid state.", new[] { nameof(MailingAddressState)});
}
if (BillingAddressCountry == "MEX" && !AddressConstants.MEXSTATES.Contains(BillingAddressState))
{
yield return new ValidationResult("Mexico addresses require a valid state.", new[] { nameof(BillingAddressState)});
}
}
}