Built-in Data Annotations

1. [RegularExpression] and Compiled Regex

Question 1: How does the [RegularExpression] attribute handle compiled regex versus runtime regex in modern .NET (.NET 7/8+)?

The built-in [RegularExpression] attribute uses the older runtime Regex engine under the hood, though it is cached. It cannot currently accept C# 11’s [GeneratedRegex] (compile-time regex) directly because attributes require constant values, and RegexGenerator creates a method. To use source-generated regex for validation, you must write a custom validation attribute or use FluentValidation.

2. Conditional Validation

Question 2: Is it possible to conditionally apply a built-in Data Annotation (e.g., make a field [Required] only if another field is true) without writing custom validation logic?

[!warning] Limitation

There is no built-in attribute like [RequiredIf]. You cannot conditionally apply data annotations out of the box. To achieve this, you must either implement IValidatableObject on the class, write a custom ValidationAttribute, or switch to FluentValidation.

3. [StringLength] vs [MaxLength]

Question 3: What is the exact behavioral difference between [StringLength] and [MaxLength] when used for MVC Model Validation versus Entity Framework Core database generation?

  • [StringLength]: Specifically designed for UI/API validation. It dictates the maximum length of the string during model binding.
  • [MaxLength]: Primarily used by Entity Framework Core to define the database schema (e.g., generating an nvarchar(50) column). MVC validation will also respect it, but [StringLength] is the idiomatic choice for API boundaries.

4. How [EmailAddress] works

Question 4: How does the built-in [EmailAddress] attribute actually validate an email? Does it perform a DNS check, or is it strictly regex-based, and is it fully RFC compliant?

It performs a very loose Regex check (essentially ensuring there is an @ symbol and some text). It is not fully RFC 5322 compliant, nor does it perform DNS lookups or SMTP verification. It strictly prevents basic user typos.

5. C# 11 required vs [Required]

Question 5: With the introduction of C# 11, how does the required keyword modifier interact with ASP.NET Core model validation compared to the traditional [Required] attribute?

  • required modifier: Enforced by the compiler and the JSON deserializer (System.Text.Json). If missing, the request fails at the deserialization phase (throwing an exception) before model validation even starts.
  • [Required] attribute: Evaluated during the model validation phase. The object is instantiated, and then the validation engine checks if the property is null.

6. Null values on non-required attributes

Question 6: How does the validation engine handle null values for attributes that are not [Required] (e.g., [StringLength(50)] on a null string)? Does it skip the check or fail it?

If a property is missing [Required], other attributes like [StringLength(50)] or [RegularExpression] skip validation on null values. The framework assumes that if it’s not required, a null is a valid state, and secondary checks shouldn’t fail it.

Custom Validation Attributes

7. IsValid(object) vs IsValid(object, ValidationContext)

Question 7: When creating a custom ValidationAttribute, what is the functional difference between overriding IsValid(object value) versus IsValid(object value, ValidationContext validationContext)?

  • IsValid(object): Only gives you the value of the specific property being validated.
  • IsValid(object, ValidationContext): Gives you the ValidationContext, which provides access to the entire object graph (parent object) and the application’s Dependency Injection container.

8. Accessing the DI Container

Question 8: How can you access the HttpContext or resolve services from the Dependency Injection (DI) container from inside a custom ValidationAttribute?

You can resolve services directly inside a custom attribute using the validation context:

protected override ValidationResult IsValid(object value, ValidationContext context)
{
    var dbContext = context.GetService(typeof(AppDbContext)) as AppDbContext;
    // Perform check...
}
 

9. Accessing Sibling Properties

Question 9: If you need to validate a property based on the value of a sibling property in the same class (e.g., EndDate must be after StartDate), how do you extract that sibling property’s value within your custom attribute?

Use context.ObjectInstance to grab the parent class being validated:

var model = (CreateEventDto)context.ObjectInstance;
if (model.EndDate < model.StartDate) 
{
    return new ValidationResult("End date must be later.");
}
 

10. Async Custom Attributes

Question 10: Can a custom ValidationAttribute be asynchronous (e.g., doing a quick database lookup to check if a username is unique)? Why or why not?

[!warning] Blocking Code

No. ValidationAttribute is strictly synchronous. You cannot use await. If you try to run .GetAwaiter().GetResult() inside a custom attribute to query a database, you risk thread starvation. For async validation (like checking if a username exists in the DB), you should use FluentValidation.

IValidatableObject & Validation Context

11. Yielding Multiple Results

Question 11: In the IValidatableObject.Validate method, what is the advantage of yielding multiple ValidationResult objects instead of just returning a single one?

Yielding allows you to return all business logic errors at once. Instead of the client fixing one error, submitting, and getting hit with another error, they receive a comprehensive array of all mistakes in a single 400 response.

12. Mapping Errors to Specific UI Fields

Question 12: How do you correctly map a ValidationResult generated inside IValidatableObject to a specific property name so the frontend knows exactly which UI field failed?

Pass the property name as an array in the ValidationResult constructor. This maps the error directly to the JSON key in the response:

yield return new ValidationResult("Date is invalid", new[] { nameof(StartDate) });
 

13. Short-circuiting Execution

Question 13: If a model implements IValidatableObject, but the parent object containing this model fails its own basic Data Annotation checks, does the child’s Validate method still execute?

If any property-level Data Annotation (like [Required]) fails, the framework skips calling IValidatableObject.Validate() entirely. It assumes the object is in a fundamentally invalid state, preventing complex business logic from crashing due to nulls or malformed data.

14. Resolving Scoped DI securely

Question 14: How can you utilize the ValidationContext provided to the Validate method to resolve scoped DI services (like a database repository) securely?

Similar to custom attributes, use validationContext.GetService(). Because the ValidationContext is created per-request by the ASP.NET Core pipeline, resolving scoped services (like a database repository) here is safe and uses the current HTTP request scope.

Complex Types & Collections

15. Validating Inside a List

Question 15: When validating a List of complex objects, do you need to add any special attributes to the list property to ensure the internal properties of T are also validated recursively?

You don’t need special attributes. The ValidationVisitor in ASP.NET Core automatically traverses IEnumerable properties and recursively validates the data annotations on the complex objects inside the list.

16. JSON Error Key Formatting for Arrays

Question 16: If a validation error occurs inside the 3rd item of a list payload, how does the ModelStateDictionary format the error key for that specific item in the JSON response?

The ModelStateDictionary formats the error key using zero-based index notation. If the 3rd user’s email fails, the error key returned in the JSON problem details will be “Users[2].Email”.

17. Enforcing “At Least One Item”

Question 17: How can you enforce that a list or array payload contains at least one item using built-in Data Annotations? Is there a built-in attribute for this, or do you need a custom one?

You can use a combination of [Required] (to ensure the list isn’t null) and [MinLength(1)] (to ensure the array/list has at least one element).

Localization & Formatting

18. Global Localization

Question 18: How do you globally localize standard validation error messages (e.g., supporting both English and Bengali error messages) in ASP.NET Core?

You achieve this by registering .AddDataAnnotationsLocalization() in your Program.cs. You then create .resx files (e.g., SharedResource.bn.resx for Bengali) where the keys are the default error messages and the values are the localized translations.

19. ErrorMessageResourceType

Question 19: What is the specific role of ErrorMessageResourceType and ErrorMessageResourceName when applying data annotations for localization?

[!info] Legacy Approach

Before global localization was introduced, you had to explicitly point every single attribute to a specific resource class and property: [Required(ErrorMessageResourceType = typeof(MyResxClass), ErrorMessageResourceName = “ReqKey”)] This is rarely needed in modern .NET apps.

20. Dynamic String Formatting

Question 20: Can you dynamically format the default error message string (e.g., injecting the invalid value provided by the user directly into the error response) using built-in attributes?

Yes, built-in attributes support positional arguments.

  • {0} maps to the Property Name.
  • {1} maps to the primary attribute argument (e.g., Max Length).
  • {2} maps to the secondary argument (e.g., Min Length). Example: [StringLength(50, MinimumLength = 5, ErrorMessage = “{0} length must be between {2} and {1}”)].

FluentValidation (The Industry Standard)

21. Clean Architecture Alignment

Question 21: From a Clean Architecture perspective, why do many modern .NET teams prefer the third-party FluentValidation library over built-in Data Annotations?

Data Annotations force you to put HTTP validation logic (and error messages) directly inside your Domain Entities or DTOs, violating the Single Responsibility Principle. FluentValidation keeps rules in completely separate classes (UserValidator : AbstractValidator), keeping models clean.

22. Handling Async Validation

Question 22: How does FluentValidation natively handle asynchronous validation rules (like querying a database for duplicates) compared to the limitations of standard Data Annotations?

FluentValidation natively supports MustAsync. The pipeline uses ValidateAsync, which properly awaits database calls without blocking threads, making it the superior choice for unique constraints or cross-referencing data.

23. DI Registration

Question 23: How are FluentValidation validator classes registered with the Dependency Injection container, and how does ASP.NET Core automatically discover them?

You typically use the FluentValidation.DependencyInjectionExtensions package and call builder.Services.AddValidatorsFromAssemblyContaining(). (Note: ASP.NET Core auto-validation integration is being phased out in modern architectural patterns; manual validation or endpoint filters are preferred).

24. RuleSets

Question 24: What is a FluentValidation RuleSet, and how can it be used to apply entirely different validation rules for the same DTO based on context (e.g., Create versus Update operations)?

A RuleSet allows you to group rules. You can use the exact same DTO for a POST and a PUT, but execute different rules.

RuleSet("Update", () => {
    RuleFor(x => x.Id).GreaterThan(0);
});
 

Minimal APIs & Advanced Scenarios

25. Standard Validation for Minimal APIs

Question 25: Since Minimal APIs do not support ModelState or Action Filters out of the box, what is the standard approach to implementing automatic validation for them?

Minimal APIs lack MVC’s Action Filters, so they don’t auto-validate. The standard approach is to use a library like MiniValidation or create an IEndpointFilter that injects a FluentValidation validator, runs it, and returns TypedResults.ValidationProblem() if errors exist.

26. The EndpointFilter Pipeline

Question 26: What is the EndpointFilter pipeline in Minimal APIs, and how can it be used to replicate the automatic 400 Bad Request validation behavior of MVC controllers?

An EndpointFilter wraps a specific Minimal API route (like a mini-middleware). You can intercept the incoming arguments, run validation on them, and short-circuit the request by returning a 400 Bad Request before your actual route handler executes.

27. C# record Validation

Question 27: How does the validation engine behave when validating C# record types or immutable objects where properties are initialized exclusively via primary constructors?

Attributes placed on the primary constructor parameters of a record are automatically compiled as attributes on the generated properties. The validation engine treats them exactly like standard classes.

28. Using IObjectModelValidator in Background Services

Question 28: What is the ObjectValidator class in ASP.NET Core, and how could you use it to manually trigger the model validation pipeline inside a background service (e.g., an IHostedService) where there is no HTTP request?

If you are in a Background Service (RabbitMQ consumer, Quartz job) with no HTTP context, you can resolve IObjectModelValidator from DI. You manually call its Validate method, passing an empty ActionContext and your object, to trigger the exact same validation engine MVC uses.

29. Polymorphic Binding Validation

Question 29: If you are using polymorphic data binding (e.g., the endpoint accepts a base class Payment, but the client sends a derived class CreditCardPayment), how does the validation engine ensure the derived class’s specific attributes are evaluated?

The ValidationVisitor evaluates the runtime type of the object, not the static compile-time type. If your API accepts a Payment base class, but the binder instantiated a CreditCardPayment, all the validation attributes specific to the Credit Card class will correctly trigger.

30. Preventing Circular Reference Crashes

Question 30: How does the ASP.NET Core validation engine prevent StackOverflowException (infinite loops) if your object graph contains circular references (e.g., a Parent has a list of Children, and each Child has a reference back to the Parent)?

The ASP.NET Core validation engine keeps track of visited object references in the current validation context. If it detects that it is about to validate an object instance it has already visited in the same graph, it breaks the loop to prevent a StackOverflowException.