Exam Area: Area 3 – Website Implementation & Delivery (25%)
Reference: https://docs.developers.optimizely.com/content-management-system/docs/visitor-groups
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);
}
}
[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();
}
// 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
}
}
@* 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>
}
// 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
// ⚠️ 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
/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
// 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;
}
}
CriterionBase<T> implement? (IsMatch(IPrincipal, HttpContext))[VisitorGroupCriterion] attribute require? (Category and DisplayName)CriterionModelBase)[VisitorGroupCriterion] attribute + class scanning)