Exam Area: Content Area 5 – Content Models (25%)
Reference: https://docs.developers.optimizely.com/content-management-system/docs/validation
using System.ComponentModel.DataAnnotations;
[ContentType(DisplayName = "Article Page", GUID = "...")]
public class ArticlePage : PageData
{
// Required
[Required(ErrorMessage = "Heading is required")]
public virtual string Heading { get; set; }
// Length validation
[StringLength(160, MinimumLength = 10, ErrorMessage = "Meta description must be 10-160 chars")]
public virtual string MetaDescription { get; set; }
// Regex
[RegularExpression(@"^\d{4}$", ErrorMessage = "Year must be 4 digits")]
public virtual string PublishYear { get; set; }
// Range
[Range(1, 100, ErrorMessage = "Priority must be 1-100")]
public virtual int Priority { get; set; }
// Email format
[EmailAddress(ErrorMessage = "Invalid email format")]
public virtual string ContactEmail { get; set; }
// URL format
[Url(ErrorMessage = "Invalid URL format")]
public virtual string ExternalUrl { get; set; }
}
using EPiServer.Core;
using EPiServer.Validation;
// Implement IValidate<T> to add custom validation for a content type
public class ArticlePageValidator : IValidate<ArticlePage>
{
public IEnumerable<ValidationError> Validate(ArticlePage instance)
{
var errors = new List<ValidationError>();
// Validate: article must have content before publishing
if (instance.Status == VersionStatus.Published)
{
if (instance.MainBody == null || string.IsNullOrEmpty(instance.MainBody.ToString()))
{
errors.Add(new ValidationError
{
ErrorMessage = "Main body is required before publishing",
PropertyName = nameof(ArticlePage.MainBody),
Severity = ValidationErrorSeverity.Error,
ValidationType = ValidationErrorType.PropertyValidation
});
}
}
// Validate: if CTA text is provided, CTA link is required
if (!string.IsNullOrEmpty(instance.CTAText) && ContentReference.IsNullOrEmpty(instance.CTALink))
{
errors.Add(new ValidationError
{
ErrorMessage = "CTA Link is required when CTA Text is provided",
PropertyName = nameof(ArticlePage.CTALink),
Severity = ValidationErrorSeverity.Warning
});
}
return errors;
}
}
public class ValidationError
{
public string ErrorMessage { get; set; } // Error text
public string PropertyName { get; set; } // Which property
public ValidationErrorSeverity Severity { get; set; } // Error/Warning/Info
public ValidationErrorType ValidationType { get; set; } // Type
public IContent RelatedContent { get; set; } // Related content
}
public enum ValidationErrorSeverity
{
Info = 0, // Info, does not block
Warning = 1, // Warning, does not block but is displayed
Error = 2 // Error, blocks save/publish
}
// Validator is automatically scanned if it implements IValidate<T>
// Or register manually in DI:
services.AddSingleton<IValidate<ArticlePage>, ArticlePageValidator>();
// Validator can inject services via constructor:
public class ArticlePageValidator : IValidate<ArticlePage>
{
private readonly IContentRepository _contentRepository;
public ArticlePageValidator(IContentRepository contentRepository)
{
_contentRepository = contentRepository;
}
public IEnumerable<ValidationError> Validate(ArticlePage instance)
{
// Use _contentRepository here...
yield break;
}
}
// DataAnnotations: simple field-level validation
[Required]
[StringLength(200)]
public virtual string Heading { get; set; }
// IValidate: cross-field, business logic validation
public class ArticleValidator : IValidate<ArticlePage>
{
public IEnumerable<ValidationError> Validate(ArticlePage instance)
{
// Complex cross-field check
if (instance.StartPublish.HasValue && instance.StopPublish.HasValue)
{
if (instance.StartPublish.Value >= instance.StopPublish.Value)
{
yield return new ValidationError
{
ErrorMessage = "Stop publish date must be after start publish date",
PropertyName = nameof(ArticlePage.StopPublish),
Severity = ValidationErrorSeverity.Error
};
}
}
}
}
Validation runs automatically when:
Editor saves page
│
├── DataAnnotations validation
├── IValidate<T> validation
│
├── Errors found? → Show in UI, block action
└── Warnings found? → Show in UI, allow action
// Create custom validation attribute
public class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
if (value is DateTime date && date <= DateTime.Today)
{
return new ValidationResult("Date must be in the future");
}
return ValidationResult.Success;
}
}
// Usage
[FutureDate]
public virtual DateTime? LaunchDate { get; set; }
IValidate<T> used for? (Custom validation logic for content type T)ValidationErrorSeverity.Error differ from .Warning? (Error blocks save; Warning is displayed but allows saving)[Required] vs IValidate<T>? (Required: simple; IValidate: complex business logic)services.AddSingleton<IValidate<T>, TValidator>())