Developer forum

Forum » Development » Extend Smart Search options in DW10

Extend Smart Search options in DW10

Marie Louise Veigert
Reply

Hi,

I have client who would like to create a smart search where one criteria is ex "watched 'product A' 7 days ago"
I know the info is possible to get from the trackingservice, but the client would like to setup these queries by themselves when needed.

The options in here - are it possible to extend that? Or any other ideas to solve the request?

I know its possible to extend the index in repositories themselves - so I thought it could be that we could do the same on smart search query.
Or do I need to do it through the "normal" queries with extending the index?

 

Best Regards
Marie Louise


Replies

 
Nicolai Pedersen Dynamicweb Employee
Nicolai Pedersen
Reply

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.

 

You must be logged in to post in the forum