🌐 Website Implementation
Visitor Groups Code
📖 Docs

Visitor Groups - Code Implementation - Optimizely CMS 12

Exam Area: Area 3 – Website Implementation & Delivery (25%)
Reference: https://docs.developers.optimizely.com/content-management-system/docs/visitor-groups


1. Custom Visitor Group Criterion

using EPiServer.Personalization.VisitorGroups;
using EPiServer.Web;

// Settings model for criterion
[DataContract(Namespace = "")]
public class UserAgentCriterionModel : CriterionModelBase
{
    [DataMember]
    [Required]
    [Display(Name = "Browser contains")]
    public string BrowserContains { get; set; }

    public override ICriterionModel Copy()
    {
        return base.ShallowCopy();
    }
}

// Criterion class
[VisitorGroupCriterion(
    Category = "Technical",
    DisplayName = "Browser Check",
    Description = "Check if user is using specific browser")]
public class UserAgentCriterion : CriterionBase<UserAgentCriterionModel>
{
    public override bool IsMatch(
        IPrincipal principal, 
        HttpContext httpContext)
    {
        var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
        return userAgent.Contains(
            Model.BrowserContains, 
            StringComparison.OrdinalIgnoreCase);
    }
}

2. CriterionBase<T> Pattern

[VisitorGroupCriterion(
    Category = "Geography",
    DisplayName = "Country Match",
    Description = "Match based on user's country")]
public class CountryCriterion : CriterionBase<CountryCriterionModel>
{
    public override bool IsMatch(IPrincipal principal, HttpContext httpContext)
    {
        // Logic to determine user's country
        var requestCountry = GetCountryFromRequest(httpContext);
        return requestCountry?.Equals(Model.Country, StringComparison.OrdinalIgnoreCase) == true;
    }

    private string GetCountryFromRequest(HttpContext context)
    {
        // From header (CDN/proxy)
        return context.Request.Headers["CF-IPCountry"].FirstOrDefault() 
            ?? context.Request.Headers["X-Country-Code"].FirstOrDefault();
    }
}

[DataContract(Namespace = "")]
public class CountryCriterionModel : CriterionModelBase
{
    [DataMember]
    [Required]
    [Display(Name = "Country code (e.g. SE, US, GB)")]
    public string Country { get; set; }

    public override ICriterionModel Copy() => base.ShallowCopy();
}

3. IVisitorGroupRole - Programmatic Check

// Check visitor group membership in code
public class PersonalizationService
{
    private readonly IVisitorGroupRoleRepository _visitorGroupRoleRepository;
    private readonly IVisitorGroupRepository _visitorGroupRepository;

    public PersonalizationService(
        IVisitorGroupRoleRepository visitorGroupRoleRepository,
        IVisitorGroupRepository visitorGroupRepository)
    {
        _visitorGroupRoleRepository = visitorGroupRoleRepository;
        _visitorGroupRepository = visitorGroupRepository;
    }

    public bool IsUserInGroup(IPrincipal principal, string groupName)
    {
        var group = _visitorGroupRepository.ListByName(groupName).FirstOrDefault();
        if (group == null) return false;
        
        return principal.IsInRole(group.Name);  // CMS handles group evaluation
    }
}

4. Personalized Content in Views

@* Render personalized blocks *@
@Html.PropertyFor(m => m.CurrentPage.MainContentArea)

@* Manual personalization check *@
@if (User.IsInRole("VisitorGroup:Returning Customers"))
{
    <div class="returning-customer-banner">Welcome back!</div>
}

5. AllowedTypes + Personalization

// ContentArea supports personalization automatically
[AllowedTypes(typeof(HeroBlock), typeof(BannerBlock))]
public virtual ContentArea PersonalizedArea { get; set; }
// Editors can add blocks with visitor group restrictions directly in the Edit UI

6. Caching with Visitor Groups

// ⚠️ Output caching must be disabled or varied by visitor group when using personalization

// Option 1: Disable output cache for pages with personalization
[ResponseCache(NoStore = true)]
public class PersonalizedController : PageController<PersonalizedPage>
{ }

// Option 2: Vary by visitor group
// CMS handles this automatically when using ICacheContext

7. Visitor Group Settings - Admin

/episerver/cms/admin → Visitor Groups
  → Create new Visitor Group
  → Add criteria (built-in + custom)
  → Set name and description
  → Apply to content blocks in Edit View

8. Dependency Injection in Criterion

// Inject services into a criterion
[VisitorGroupCriterion(Category = "Commerce", DisplayName = "Customer Segment")]
public class CustomerSegmentCriterion : CriterionBase<CustomerSegmentModel>
{
    private readonly ICustomerService _customerService;

    // Use ServiceLocator because Criterion instances are created by the CMS
    private ICustomerService CustomerService =>
        ServiceLocator.Current.GetInstance<ICustomerService>();

    public override bool IsMatch(IPrincipal principal, HttpContext httpContext)
    {
        var userId = principal?.Identity?.Name;
        if (string.IsNullOrEmpty(userId)) return false;

        var segment = CustomerService.GetSegment(userId);
        return segment == Model.RequiredSegment;
    }
}

Review Questions

  1. Which method must CriterionBase<T> implement? (IsMatch(IPrincipal, HttpContext))
  2. What does the [VisitorGroupCriterion] attribute require? (Category and DisplayName)
  3. What must the criterion model (T) inherit from? (CriterionModelBase)
  4. Why do Visitor Groups affect caching? (Each visitor may see different content → cannot share a single cached response)
  5. How is a custom criterion automatically registered? (Via the [VisitorGroupCriterion] attribute + class scanning)