Yes, it is extensible. Here’s the full picture:
Extension mechanism The field list comes from UserSmartSearchDataProvider.OnGetRulesDataFields(), which aggregates three sources:
Predefined fields — hardcoded in the provider itself Custom user fields — dynamic user-defined fields Extender fields — from any class implementing UserSmartSearchRule, loaded automatically via AddInManager How to add new fields Create a class that extends UserSmartSearchRule. The AddInManager will discover and load it automatically — no registration needed.
public sealed class MyCustomUserSmartSearchRule : UserSmartSearchRule
{
public override List<SmartSearchRuleField> GetRuleFields()
{
return new List<SmartSearchRuleField>
{
new SmartSearchRuleField(
id: "MyField",
name: "My Custom Field",
typeName: typeof(string).FullName,
controlType: SmartSearchRuleControlType.TextBox,
groupName: "My Group"
)
};
}
public override string GetFilter(SmartSearchRule rule)
{
// Return SQL WHERE clause fragment based on rule.Operator and rule.Value
return $"[SomeColumn] = '{rule.Value}'";
}
// Optionally override GetTableNames() if you need extra JOIN tables
}
The GetFilter method is responsible for translating the rule into a SQL filter. Look at EcommerceUserSmartSearchRule in Dynamicweb.Ecommerce/SmartSearchProviders/EcommerceUserSmartSearchRule.cs for a full reference implementation with multiple fields, operators, and table joins.
Example
Attached the implementation running the ecommerce related fields in smart search that you can base an implementation on.
Additional information:
Here's a concrete walkthrough using a simplified but realistic example --- imagine you want to add a "Last login was before/after date" and "Login count >= N" rule:
How GetFilter and GetTableNames work
The contract
The SQL engine builds a query roughly like:
SELECT AccessUserID FROM AccessUser
{joins from GetTableNames()}
WHERE
{filter from GetFilter()}
AND {filter from GetFilter()} -- one per rule in the smart search
So GetTableNames adds LEFT OUTER JOINs to bring in extra tables, and GetFilter returns the WHERE fragment for one rule.
Simple case --- filtering a column already on AccessUser
No join needed. GetTableNames returns an empty list, GetFilter reads directly from the AccessUser table.
public sealed class LoginActivitySmartSearchRule : UserSmartSearchRule
{
public LoginActivitySmartSearchRule()
{
PREFIX = "_custom_login_";
}
private const string LastLogin = "last_login";
private const string LoginCount = "login_count";
public override List<SmartSearchRuleField> GetRuleFields() =>
[
new SmartSearchRuleField(
PREFIX + LastLogin,
"Last Login Date",
typeof(DateTime).FullName!,
controlType: SmartSearchRuleControlType.DateCtrl,
groupName: "Login Activity"),
new SmartSearchRuleField(
PREFIX + LoginCount,
"Total Login Count",
typeof(int).FullName!,
controlType: SmartSearchRuleControlType.NumericBox,
groupName: "Login Activity"),
];
// No extra tables needed --- both columns live on AccessUser already
public override List<string> GetTableNames(SmartSearchRule rule) => [];
public override string GetFilter(SmartSearchRule rule)
{
var action = GetAction(rule);
var sql = action switch
{
// AccessUser.AccessUserLastLogin is the column name in the DB
LastLogin => SmartSearchSqlQueryHelper.GetDateWherePart("AccessUser", "AccessUserLastLogin", rule),
LoginCount => SmartSearchSqlQueryHelper.GetIntegerWherePart("AccessUserLoginCount", rule),
_ => "0 = 1" // fallback: never match unknown actions
};
return $"({sql})";
}
}
GetDateWherePart("AccessUser", "AccessUserLastLogin", rule) translates the rule operator + value into e.g. AccessUser.AccessUserLastLogin > CONVERT(datetime, '2024-01-01', 120).
Complex case --- filtering via a JOIN to another table
When the data you need lives in a separate table, GetTableNames adds the join and GetFilter references the aliased table name.
public sealed class NewsletterSmartSearchRule : UserSmartSearchRule
{
public NewsletterSmartSearchRule()
{
PREFIX = "_custom_newsletter_";
}
private const string SubscribedDate = "subscribed_date";
public override List<SmartSearchRuleField> GetRuleFields() =>
[
new SmartSearchRuleField(
PREFIX + SubscribedDate,
"Newsletter Subscription Date",
typeof(DateTime).FullName!,
controlType: SmartSearchRuleControlType.DateCtrl,
groupName: "Newsletter"),
];
public override List<string> GetTableNames(SmartSearchRule rule)
{
if (GetAction(rule) != SubscribedDate)
return [];
// GetTableName(actionName, realTableName, addAlias: true) produces
// "NewsletterSubscriptions as _custom_newsletter_subscribed_dateNewsletterSubscriptions"
// which is a unique alias that won't clash with other joins.
var aliasedTable = GetTableName(SubscribedDate, "NewsletterSubscriptions", addAlias: true);
var alias = GetTableName(SubscribedDate, "NewsletterSubscriptions"); // just the alias
return [$"LEFT OUTER JOIN {aliasedTable} ON {alias}.UserID = AccessUser.AccessUserID"];
}
public override string GetFilter(SmartSearchRule rule)
{
var alias = GetTableName(SubscribedDate, "NewsletterSubscriptions"); // same alias as above
var sql = SmartSearchSqlQueryHelper.GetDateWherePart(alias, "SubscribedOn", rule);
return $"({sql})";
}
}
The key insight: GetTableName(action, tableName) produces a deterministic alias from your PREFIX + action + real table name. You call it in both GetTableNames (to declare the join) and GetFilter (to reference the alias in the WHERE clause) --- they must match exactly.
Subquery case --- when you need aggregation
If you need a value like "total spend", you can't just join --- you need a subquery that aggregates per user. The join becomes a derived table:
case TotalSpend:
{
// Subquery aggregates in GetTableNames, result column referenced in GetFilter
var alias = GetTableName(TotalSpend, "EcomOrders"); // alias for the subquery
return [$@"LEFT OUTER JOIN (
SELECT OrderCustomerAccessUserID, SUM(OrderPriceWithVAT) AS total_spend
FROM EcomOrders
WHERE OrderComplete = 1
GROUP BY OrderCustomerAccessUserID
) AS {alias} ON {alias}.OrderCustomerAccessUserID = AccessUser.AccessUserID"];
}
Then in GetFilter:
case TotalSpend:
{
var alias = GetTableName(TotalSpend, "EcomOrders");
return $"({SmartSearchSqlQueryHelper.GetFloatWherePart(alias, "total_spend", rule)})";
}
Key helpers at a glance
| Helper |
Use for |
GetDateWherePart(table, column, rule) |
Date columns with operator + value |
GetIntegerWherePart(column, rule) |
Integer columns (no table prefix) |
GetIntegerWherePart(table, column, rule) |
Integer columns on a joined table |
GetFloatWherePart(column, rule) |
Float/decimal columns |
GetStringWherePart(column, rule) |
Text columns, supports LIKE |
GetBooleanWherePart(column, rule) |
Bit/bool columns |
RenderValue(value) |
Safely escape a value for inline SQL |
RenderOperator(rule.Operator) |
Translate operator enum to =, >, < etc. |
All helpers handle the full set of SmartSearchRuleOperator values (Equals, NotEqualTo, IsEmpty, IsInRange, IsBefore, IsAfter, etc.) so you don't have to write the operator switch yourself.