📋 Content Models
Validations
📖 Docs

Validations - Optimizely CMS 12

Exam Area: Content Area 5 – Content Models (25%)
Reference: https://docs.developers.optimizely.com/content-management-system/docs/validation


1. DataAnnotations 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; }
}

2. IValidate<T> Interface (CMS-specific Validation)

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;
    }
}

3. ValidationError Properties

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
}

4. ValidationErrorSeverity Enum

public enum ValidationErrorSeverity
{
    Info = 0,     // Info, does not block
    Warning = 1,  // Warning, does not block but is displayed
    Error = 2     // Error, blocks save/publish
}

5. Registering a Validator

// 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;
    }
}

6. Combine DataAnnotations + IValidate

// 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
                };
            }
        }
    }
}

7. Validation in Edit Mode

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

8. Custom Attribute Validation

// 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; }

Review Questions

  1. What is IValidate<T> used for? (Custom validation logic for content type T)
  2. How does ValidationErrorSeverity.Error differ from .Warning? (Error blocks save; Warning is displayed but allows saving)
  3. When to use [Required] vs IValidate<T>? (Required: simple; IValidate: complex business logic)
  4. How is a validator registered? (Automatically via scan or services.AddSingleton<IValidate<T>, TValidator>())
  5. When does validation run? (When the editor clicks Save or Publish)