6

I've made a custom rule that enables the user to get a date field from any facet and see if that date's passed. I've made two variants, one for personalization and one for list segmentation/Marketing Automation. They use the same code to get the facet value. The personalization rule works fine; however when I try to segment a list an error is shown

list segmentation error

and the log shows this beauty (YourLinqIsTooStrongException):

11856 17:37:43 ERROR Application error.
Exception: System.Web.HttpUnhandledException
Message: Exception of type 'System.Web.HttpUnhandledException' was thrown.
Source: System.Web
   at System.Web.UI.Page.HandleError(Exception e)
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
   at System.Web.UI.Page.ProcessRequest()
   at System.Web.UI.Page.ProcessRequest(HttpContext context)
   at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
   at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
   at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Nested Exception

Exception: System.Reflection.TargetInvocationException
Message: Exception has been thrown by the target of an invocation.
Source: mscorlib
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at Sitecore.Reflection.ReflectionUtil.InvokeMethod(MethodInfo method, Object[] parameters, Object obj)
   at Sitecore.Nexus.Pipelines.NexusPipelineApi.Resume(PipelineArgs args, Pipeline pipeline)
   at Sitecore.Web.UI.Sheer.ClientPage.ResumePipeline()
   at Sitecore.Web.UI.Sheer.ClientPage.OnPreRender(EventArgs e)
   at System.Web.UI.Control.PreRenderRecursiveInternal()
   at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

Nested Exception

Exception: Sitecore.XConnect.Search.YourLinqIsTooStrongException
Message: Unsupported query: (VisitMethodCall): value(VGZ.Sitecore.Foundation.MarketingAutomation.Rules.ExmDateCondition).Evaluate([contact])
Source: Sitecore.Xdb.Common.Web
   at Sitecore.Xdb.Common.Web.Synchronous.SynchronousExtensions.SuspendContextLock[TResult](Func\`1 taskFactory)
   at Sitecore.XConnect.Client.XConnectSynchronousExtensions.SuspendContextLock[TResult](Func\`1 taskFactory)
   at Sitecore.XConnect.Client.XConnectSynchronousExtensions.GetBatchEnumeratorSync[TEntity](IAsyncQueryable\`1 asyncQueryable, Int32 defaultBatchSize)
   at Sitecore.XConnect.Segmentation.SegmentationEngine.<>c.<CountContacts>b__24_0(IAsyncQueryable\`1 contacts)
   at Sitecore.XConnect.Segmentation.SegmentationEngine.PerformSearch[T](IContactSearchQueryFactory queryFactory, Func\`2 searchWork, Byte[] syncToken, ContactExpandOptions expandOptions)
   at Sitecore.XConnect.Segmentation.SegmentationEngine.CountContacts(IContactSearchQueryFactory queryFactory)
   at Sitecore.Shell.Applications.Analytics.SegmentBuilder.SegmentBuilderForm.EditFilterCondition(ClientPipelineArgs args)

The class that's giving me headaches:

public class ExmDateCondition : ICondition, IMappableRuleEntity, IContactSearchQueryFactory
{
    /// <summary>Gets or sets the facet that contains a date.</summary>
    /// <value>facet.value</value>
    public string DateFacet { get; set; }

    public Expression<Func<Contact, bool>> CreateContactSearchQuery(IContactSearchQueryContext context)
    {
        return contact => Evaluate(contact);
    }

    public bool Evaluate(IRuleExecutionContext context)
    {
        return Evaluate(context.Fact<Contact>());
    }

    private bool Evaluate(Contact contact)
    {
        if (string.IsNullOrWhiteSpace(DateFacet))
        {
            Log.Info(GetType() + ": DateFacet is null", this);
            return false;
        }

        string[] propertyPath = DateFacet.Split('.');
        if (propertyPath.Length == 0)
        {
            Log.Info(GetType() + ": facet path is empty", this);
            return false;
        }

        DateTime utcNow = DateTimeProvider.GetUtcNow();

        var values = FacetProvider.GetFacetValue<DateTime>(contact, propertyPath);
        if (utcNow >= values.ToUniversalTime())
        {
            return true;
        }

        // maybe the facet value was nullable datetime
        var value = FacetProvider.GetFacetValue<DateTime?>(contact, propertyPath);
        return value.HasValue && utcNow >= value.Value.ToUniversalTime();
    }
}

They're both configured with this rule text:

when the date in [DateFacet,Text,,facetpath (Facet.ValueName)] has passed

Using the rule in a Marketing Automation listener doesn't generate an error but nothing seems to happen either.

Has anyone encountered this error before and how do I resolve it?

I can imagine it has something to do with Sitecore not liking the complexity of my rule, but I'd very much like to be able to query a facet value in a generic way like this.

EDIT: Even after updating my code to directly call GetFacetValue(), Sitecore now trips over that method. The solution was to let go of trying to make a generic rule and making separate rules for the facet values we need.

public Expression<Func<Contact, bool>> CreateContactSearchQuery(IContactSearchQueryContext context)
{
    return contact => FacetProvider.GetFacetValue<DateTime>(contact, DateFacet.Split('.')) >= DateTimeProvider.GetUtcNow() || FacetProvider.GetFacetValue<DateTime?>(contact, DateFacet.Split('.')) >= DateTimeProvider.GetUtcNow();
}

resulted in

Exception: Sitecore.XConnect.Search.YourLinqIsTooStrongException
Message: Unsupported query: (VisitMethodCall): GetFacetValue([contact], value(System.String[]))
  • 2
    See doc.sitecore.net/developers/xp/xconnect/xconnect-client-api/…. You cannot to || inside your Any(), or a nested Any() – Jacob Nielsen May 29 '18 at 10:53
  • Thanks! So, how restrictive is this? I can rewrite the method to not use Any(), but can I also not use || within the method that's called? – Mark-Danney May 29 '18 at 11:32
  • I use SOLR search locally, but the no || in Any() limitation only seems to be on Azure search. Might there be another reason I'm getting this error? I've updated my code to not use Any() at all anymore but the same error persists. – Mark-Danney May 29 '18 at 11:42
6

Sadly, my solution was too ambitious for the Sitecore predicate engine. The solution was to re-write the condition to a bare-bones implementation where a Facet it retrieved by strong-typed class.

public class ExmReminderEmailDateCondition : ICondition, IMappableRuleEntity, IContactSearchQueryFactory
{
    public Expression<Func<Contact, bool>> CreateContactSearchQuery(IContactSearchQueryContext context)
    {
        return contact => contact.GetMaturityMailDates().ReminderEmailDate <= DateTimeProvider.GetUtcNow();
    }

    public bool Evaluate(IRuleExecutionContext context)
    {
        var facet = context.Fact<Contact>().GetMaturityMailDates();
        if (facet == null)
        {
            return false;
        }

        return facet.ReminderEmailDate <= DateTimeProvider.GetUtcNow();
    }
}

This didn't result in a working immediately though, per the documentation I needed to register the condition in the Automation Engine after which it started working.

  • Thanks for sharing :-) Feel free to self-mark the answer when that option becomes available – Mark Cassidy May 29 '18 at 14:19
1

Update 1: example of an expression. One additional challenge there is that you need to get the type of the facet so that you can feed it into the GetContactPredicate call:

    private static Expression<Func<Contact, bool>> GetContactPredicate(Type facetType, string facetKey, string propertyName, object value)
    {
        var contact = Expression.Parameter(typeof(Contact));
        var facet = Expression.Call(contact, "GetFacet", new [] {facetType}, Expression.Constant(facetKey));
        var valueInIndex = Expression.Property(facet,  propertyName);
        var valuesComparisson = Expression.LessThanOrEqual(valueInIndex, Expression.Constant(value));
        return Expression.Lambda<Func<Contact, bool>>(valuesComparisson, contact);
    }

Sample call with hardcoded values:

    GetContactPredicate(typeof(MaturityMailDates), MaturityMailDates.DefaultFacetKey, "ReminderEmailDate", DateTimeProvider.GetUtcNow());

The problem is XConnect doesn’t support custom method calls in its linq query expressions.

This prevents using any kind of special logic to get to those facet properties to be compared.

What you can do instead, is drop down to issuing the Expression directly so that it can still be a general solution without having the need to have the method calls within the Expression passed to XConnect.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.