---
title: Create custom audience criteria
slug: cms-12/docs/developing-custom-visitor-group-criteria
docTags: 
createdAt: 2026-08-10T15:56:02.000Z
---

Optimizely comes with several predefined audience criteria, but you can develop your own.

Each criterion defines a condition evaluated to determine if a visitor is part of a group. The visitor is considered a group member if sufficient audience criteria are fulfilled. The following example shows a **Query string** criterion that is fulfilled if the query string search has the value audiences:

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/c1p6p3NBYBPZtTxGk8PL8_96dc3cc-querystringcriteria.png)

From a development standpoint, an audience criterion is a combination of (at least) two classes:

- A `model class`, which stores and persists user input from the user interface.
- A `criterion class`, which evaluates the context and the data stored in the model to determine if the criteria are fulfilled.

:::hint{type="info"}
**Prerequisites**
Make sure that your project references and the following assemblies contain the classes you need to create criteria and models.
:::

## Create a model class

The model class stores and persists user input from the user interface, providing the criterion class access to the settings. The model class must implement `ICriterionModel`because instances of the model class are persisted to the Optimizely Dynamic Data Store, `IDynamicData`. The best way to implement those interfaces is to inherit from `CriterionModelBase`, which contains standard implementations.

The only method you must override is `CriterionModelBase.Copy`. If you are working with simple value-type properties, it is sufficient to let you override call `base.ShallowCopy`. If your model has reference type properties and you want to ensure that each instance has its copy of the referenced objects, you must extend the Copy override with more logic.

```csharp
public class YourModelClass : CriterionModelBase {
  public override ICriterionModel Copy() { return base.ShallowCopy(); }
}
```

Add public properties to your class corresponding to the settings you want available in the user interface, where inputs are created for the public properties. Depending on the property type, a suitable element is used when editing the criterion.

```csharp
public string MyString {
  get;
  set;
}
public int MyInt {
  get;
  set;
}
```

## CriterionPropertyEditor property attribute

You can use the attribute to control parts of the rendering of the property. `PreText` and `AfterText` are used to display strings before and after the element. For example, "Ordered within "\[element] "days". `AdditionalPropsJson` can send additional configuration to the element, like setting the type or adding inline styling. You also can set a translation for the label, whether to ignore the property or not, the default value, and ordering of the property.

```csharp
[CriterionPropertyEditor(
  PreTextTranslationKey = "/path/to/xml",
  AfterTextTranslationKey = "/path/to/xml",
  LabelTranslationKey = "/path/to/xml",
  Order = 1 DefaultValue = "0" AdditionalPropsJson = "{ \"type\" : \"time\" }")]
public string MyString {
  get;
  set;
}
```

If you want the input for a value to be a drop-down list with predefined options, set `SelectionFactoryType` on the `CriterionPropertyEditor` attribute. The `SelectionFactoryType` is a type that implements `ISelectionFactory`. The `ISelectionFactory` has a `GetSelectListItems` method that is responsible for supplying options for the drop-down list.

You can use an `EnumSelectionFactory` included in the CMS to present the options of an `Enum` in a drop-down list. When you use `EnumSelectionFactory`, provide translations of the `Enum` values and place the translated values in your XML file in the \_/lang\_directory. See Enumeration Localization in [Localizing the audience criterion](docId\:BsD8TsD7S08QmT8yA0n7S).

```csharp
[CriterionPropertyEditor(SelectionFactoryType = typeof (EnumSelectionFactory))]
public SomeEnum MyEnumSelector {
  get;
  set;
}
```

## Default criterion editors

The default criterion editor is used if you do not provide your editor with [ScriptUrl](https://world.optimizely.com/CsClassLibraries/cms/EPiServer.Personalization.VisitorGroups.VisitorGroupCriterionAttribute?version=12#EPiServer_Personalization_VisitorGroups_VisitorGroupCriterionAttribute_ScriptUrl). The following data types are supported.

| Data type                                                   | Editor                                           |
| ----------------------------------------------------------- | ------------------------------------------------ |
| `System.String`                                             | Input with default type = text                   |
| `System.Int32`                                              | Input with default type = number                 |
| `System.Single`                                             | Input with default type = number and step = 0.01 |
| `System.Boolean`                                            | Checkbox                                         |
| `EPiServer.Personalization.VisitorGroups.Criteria.PageInfo` | EPiServer Page Selector                          |
| `System.DateTime`                                           | Input with default type = date                   |

### Customize the default criterion editors with AdditionalPropsJson

The default editors are React components and properties with `AdditionalPropsJson` is parsed and sent as additional properties to those React components. You can use the following additional properties from `EPiServer CMS`. Otherwise, HTML input attributes should work as expected.

| Attribute    | Type      | Description                                                            |
| ------------ | --------- | ---------------------------------------------------------------------- |
| `textarea`   | `boolean` | Set to true to enable textarea variant                                 |
| `label`      | `string`  | Optional label text that displays as the floating label or placeholder |
| `helperText` | `string`  | Helper text that displays under the input to explain what the field is |

### Example

```csharp
[CriterionPropertyEditor(AdditionalPropsJson = "{ \"textarea\" : \"true\",  \"value\" : \"my default text\"}")]
public string MyTextArea {
  get;
  set;
}

[CriterionPropertyEditor(AdditionalPropsJson = "{ \"label\" : \"My string label\",  \"helperText\" : \"This is my helper text.\"}")]
public string MyString {
  get;
  set;
}
```

## Validate the server side

You can add input validation to your properties by using the attribute classes in `System.ComponentModel.DataAnnotations`. The following validation rules are supported:

- `[Required]`
- `[Range(double Minimum, double maximum)]`
- `[StringLength(int maximumLength)]`
- `[RegularExpression(string pattern)]`

```csharp
[Required]
[StringLength(10)]
public string MyString {
  get;
  set;
}
```

If you want to add custom server-side validation, you can implement the interface `IValidateCriterionModel` on your model. The `IValidateCriterionModel` supplies a `Validate` method that is called when you save an audience containing the criterion.

```csharp
public class YourModelClass: CriterionModelBase, IValidateCriterionModel {
  public string MyString {
    get;
    set;
  }
  public CriterionValidationResult Validate(VisitorGroup currentGroup) {
      if (MyString.Length > 5) {
        return new CriterionValidationResult(false, "MyString is too long!", "MyString");
      }
      return new CriterionValidationResult(true);
    }
    ...
}
```

## Create a criterion class

After you have a model class, create the criterion class to evaluate the context and the data stored in the model to determine whether the criteria are fulfilled. The connection between the criterion and model classes is created with `CriterionBase` – the base class that you must use for the criterion class – which is a generic class that accepts `ICriterionModel` parameters. You must override the `CriterionBase.IsMatch` method, which is the central method for a criterion; it is called when evaluating if a user is a member of an audience.

Decorate the criterion class with `VisitorGroupCriterion` attribute, which identifies your class as a criterion and makes it available for use. `VisitorGroupCriterion` has the following settings:

- `Category` – The group's name in the criteria picker user interface where this criterion is located. Criteria with the same `Category` value are grouped.
- `Description` – A text describing how the criterion works.
- `DisplayName` – A short name that identifies the criterion in menus and audiences.
- `LanguagePath` – The path in the XML language files where the strings associated with this criterion are located. See [Localize the audience criterion](docId\:BsD8TsD7S08QmT8yA0n7S).
- `ScriptUrl` – A URL referring to a JavaScript file you load when this criterion is edited.

```csharp
[VisitorGroupCriterion(
  Category = "My Criteria Category",
  Description = "How the criterion works",
  DisplayName = "Short Name",
  LanguagePath = "/xml/path/to/translations/",
  ScriptUrl = "javascript-that-should-be-loaded-for-the-UI.js")]
public class YourCriterionClass: CriterionBase<YourModelClass> {
  public override bool IsMatch(IPrincipal principal, HttpContextBase httpContext) {
    // Your evaluation code here.
    // The model class instance is available via the Model property.
  }
}
```

## Subscribe to events

You can subscribe to specific events by overriding the `Subscribe` method, which gathers information about events that occur before the call to the `IsMatch` method. For example, the built-in Visited Page criterion needs to keep track of visited pages in the current session. You can subscribe to the following events:

- `EndRequest`
- `StartRequest`
- `StartSession`
- `VisitedPage`

If you override the `Subscribe` method and attach event handlers, make sure that you also override the `Unsubscribe` method and detach the event handlers.

```csharp
public override void Subscribe(ICriterionEvents criterionEvents) {
  criterionEvents.StartRequest += criterionEvents_StartRequest;
}
public override void Unsubscribe(ICriterionEvents criterionEvents) {
  criterionEvents.StartRequest -= criterionEvents_StartRequest;
}
void criterionEvents_StartRequest(object sender, CriterionEventArgs e) {
  // Handle the event
}
```
